DKNet.EfCore.Events 10.1.2

There is a newer version of this package available.
See the version list below for details.
dotnet add package DKNet.EfCore.Events --version 10.1.2
                    
NuGet\Install-Package DKNet.EfCore.Events -Version 10.1.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DKNet.EfCore.Events" Version="10.1.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DKNet.EfCore.Events" Version="10.1.2" />
                    
Directory.Packages.props
<PackageReference Include="DKNet.EfCore.Events" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DKNet.EfCore.Events --version 10.1.2
                    
#r "nuget: DKNet.EfCore.Events, 10.1.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DKNet.EfCore.Events@10.1.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DKNet.EfCore.Events&version=10.1.2
                    
Install as a Cake Addin
#tool nuget:?package=DKNet.EfCore.Events&version=10.1.2
                    
Install as a Cake Tool

DKNet.EfCore.Events

NuGet NuGet Downloads .NET License

Enhanced Entity Framework Core event-based functionality for implementing domain-driven design (DDD) patterns. This library provides centralized event management, automatic event publishing during EF Core operations, and seamless integration with domain entities.

Features

  • Domain Event Management: Queue and publish domain events from entities
  • Automatic Event Publishing: Events fired automatically during EF Core SaveChanges
  • Event Publisher Abstraction: Central hub for event routing and handling
  • EF Core Hooks Integration: Pre and post-save event triggers
  • Custom Event Handlers: Flexible event handling with dependency injection
  • Entity Event Tracking: Track and manage events at the entity level
  • Exception Handling: Robust error handling for event processing
  • Performance Optimized: Efficient event queuing and batch processing

Supported Frameworks

  • .NET 9.0+
  • Entity Framework Core 9.0+

Installation

Install via NuGet Package Manager:

dotnet add package DKNet.EfCore.Events

Or via Package Manager Console:

Install-Package DKNet.EfCore.Events

Quick Start

Setup Event Publisher

using DKNet.EfCore.Events.Handlers;
using Microsoft.Extensions.DependencyInjection;

// Register event publisher implementation
services.AddEventPublisher<AppDbContext, EventPublisher>();

// Or use your custom implementation
public class CustomEventPublisher : IEventPublisher
{
    public async Task PublishAsync(object eventItem, CancellationToken cancellationToken = default)
    {
        // Custom event publishing logic
        await Task.CompletedTask;
    }
}

services.AddEventPublisher<AppDbContext, CustomEventPublisher>();

Domain Entity with Events

using DKNet.EfCore.Abstractions.Entities;

public class Product : Entity<Guid>
{
    public Product(string name, decimal price, string createdBy) 
        : base(Guid.NewGuid(), createdBy)
    {
        Name = name;
        Price = price;
        
        // Add domain event
        AddEvent(new ProductCreatedEvent(Id, name, price));
    }

    public string Name { get; private set; }
    public decimal Price { get; private set; }
    
    public void UpdatePrice(decimal newPrice, string updatedBy)
    {
        var oldPrice = Price;
        Price = newPrice;
        SetUpdatedBy(updatedBy);
        
        // Add domain event for price change
        AddEvent(new ProductPriceChangedEvent(Id, oldPrice, newPrice));
    }
}

// Domain events
public record ProductCreatedEvent(Guid ProductId, string Name, decimal Price);
public record ProductPriceChangedEvent(Guid ProductId, decimal OldPrice, decimal NewPrice);

Event Handlers

using DKNet.EfCore.Events.Handlers;

public class ProductCreatedHandler : INotificationHandler<ProductCreatedEvent>
{
    private readonly ILogger<ProductCreatedHandler> _logger;
    private readonly IEmailService _emailService;

    public ProductCreatedHandler(ILogger<ProductCreatedHandler> logger, IEmailService emailService)
    {
        _logger = logger;
        _emailService = emailService;
    }

    public async Task Handle(ProductCreatedEvent notification, CancellationToken cancellationToken)
    {
        _logger.LogInformation("Product created: {ProductId} - {Name} (${Price})", 
            notification.ProductId, notification.Name, notification.Price);
            
        // Send notification email
        await _emailService.SendProductCreatedNotificationAsync(notification, cancellationToken);
    }
}

public class ProductPriceChangedHandler : INotificationHandler<ProductPriceChangedEvent>
{
    private readonly IInventoryService _inventoryService;

    public ProductPriceChangedHandler(IInventoryService inventoryService)
    {
        _inventoryService = inventoryService;
    }

    public async Task Handle(ProductPriceChangedEvent notification, CancellationToken cancellationToken)
    {
        // Update inventory records
        await _inventoryService.UpdatePriceAsync(notification.ProductId, notification.NewPrice, cancellationToken);
    }
}

Configuration

DbContext Setup

Events are automatically published during SaveChanges when the event hook is registered:

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<Product> Products { get; set; }
    public DbSet<Order> Orders { get; set; }

    // Event publishing happens automatically via EventHook
}

Event Handler Registration

// Register event handlers
services.AddScoped<INotificationHandler<ProductCreatedEvent>, ProductCreatedHandler>();
services.AddScoped<INotificationHandler<ProductPriceChangedEvent>, ProductPriceChangedHandler>();

// Or use MediatR for automatic discovery
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(ProductCreatedHandler).Assembly));

Declared Domain Events ([RaisesEvent])

Declaring an event is two separate steps: shape the payload as an ordinary DtoGenerator-generated record via [GenerateDto], then declare a raise rule on the entity via the repeatable DKNet.EfCore.Abstractions.Events.RaisesEventAttribute naming that payload, the persistence operation(s) that raise it, and — for updates — an optional narrowing property list:

using DKNet.EfCore.Abstractions.Events;
using DKNet.EfCore.DtoGenerator;

[GenerateDto(typeof(Order), Exclude = new[] { "InternalNote" })]
public partial record OrderPlacedEvent;

[RaisesEvent(typeof(OrderPlacedEvent), EventOperations.Created)]
[RaisesEvent(typeof(OrderStatusChangedEvent), EventOperations.Updated, nameof(Order.Status))]
public class Order { public string Status { get; set; } = string.Empty; }

DKNet.EfCore.DtoGenerator validates [RaisesEvent] rules at build time (payload/entity match, narrowing property names) but emits no code for them — see that package's README for the diagnostics. This package (DKNet.EfCore.Events) is what raises them at runtime:

  • Automatic raising: declared events are captured before SaveChanges (so update narrowing can inspect EntityEntry.Property(...).IsModified) and published through the same IEventPublisher path as hand-raised events, after a successful save.
  • Coexistence: an entity can both declare events via [RaisesEvent] and raise events by hand via AddEvent(...) — both are published, as distinct types, from the same save.
  • No base-class requirement: any entity mapped by the DbContext may declare events; it does not need to be an AggregateRoot or implement IEventEntity.
  • Delete events carry pre-removal values: because a deleted entity's in-memory property values are untouched by the database delete, the raised delete event mirrors the entity exactly as it was before removal.
  • One raise per payload per operation: if two rules on the same entity name the same payload for the same operation, it raises once.

String-form rules

[RaisesEvent("CustomerTouched", EventOperations.Created)] names an event by string instead of an existing [GenerateDto] type — DKNet.EfCore.DtoGenerator generates the payload record for you (see its README). At runtime this package resolves the generated type by reflection from the entity's own assembly and namespace, cached per entity type + event name. If the generated record is missing (e.g. the generator wasn't referenced, or the project didn't rebuild), the first save that would raise it throws an EventException naming the missing event — never silently dropped. Everything else — capture timing, narrowing, coexistence with hand-raised events, one-raise-per-payload dedup — behaves identically to the type-naming form.

Mapping requirement

Declared events are produced by mapping the entity onto the payload type through the registered IMapper — the same mapper used for AddEvent<TEvent>() type-based mapping. Register one (e.g. Mapster's IMapper) alongside AddEventPublisher:

services.AddSingleton<IMapper, Mapper>(); // or your IMapper registration of choice
services.AddEventPublisher<AppDbContext, EventPublisher>();

Without a registered IMapper, saving an entity that raised at least one declared event throws:

Entity raised {N} declared event(s) via [RaisesEvent], which map the entity onto the event type
and therefore require an IMapper registration. Register one to use declared domain events.

Migration note

Adopting declared events on an existing domain needs: a [GenerateDto] payload record, a [RaisesEvent] rule naming it on the entity, this package (DKNet.EfCore.Events), and an IMapper registration. A domain project that only references DKNet.EfCore.Abstractions and DKNet.EfCore.DtoGenerator builds and packs fine with rules declared — nothing raises until the application also registers this package's save hook. Existing hand-raised events keep firing unchanged, and no entity needs a base-class change to start declaring events.

Nested owned-value limitation

A change confined to a nested owned value ([Owned] / OwnsOne) does not raise the owner's update event — EF Core does not report the owner itself as Modified when only an owned value changed. Narrow the rule's properties to the owner's own direct properties only.

Security note

A declared event mirrors the entity's properties by default, same as [GenerateDto] — sensitive values are included unless Excluded on the payload's [GenerateDto] declaration.

API Reference

Core Interfaces

  • IEventPublisher - Central event publishing abstraction
  • IEventEntity - Interface for entities that can raise domain events (from DKNet.EfCore.Abstractions)
  • EntityEventItem - Wrapper for entity events with metadata

Event Management

  • AddEvent(object) - Queue domain event on entity
  • ClearEvents() - Clear all queued events
  • GetEvents() - Retrieve all queued events

Setup Extensions

  • AddEventPublisher<TDbContext, TImplementation>() - Register event publisher with EF Core hooks

Advanced Usage

Custom Event Publisher

public class MediatREventPublisher : IEventPublisher
{
    private readonly IMediator _mediator;
    private readonly ILogger<MediatREventPublisher> _logger;

    public MediatREventPublisher(IMediator mediator, ILogger<MediatREventPublisher> logger)
    {
        _mediator = mediator;
        _logger = logger;
    }

    public async Task PublishAsync(object eventItem, CancellationToken cancellationToken = default)
    {
        try
        {
            _logger.LogDebug("Publishing event: {EventType}", eventItem.GetType().Name);
            
            if (eventItem is INotification notification)
            {
                await _mediator.Publish(notification, cancellationToken);
            }
            else
            {
                _logger.LogWarning("Event {EventType} does not implement INotification", eventItem.GetType().Name);
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to publish event: {EventType}", eventItem.GetType().Name);
            throw new EventException($"Failed to publish event of type {eventItem.GetType().Name}", ex);
        }
    }
}

Complex Domain Event Scenarios

public class Order : AggregateRoot
{
    private readonly List<OrderItem> _items = [];

    public Order(Guid customerId, string createdBy) : base(createdBy)
    {
        CustomerId = customerId;
        Status = OrderStatus.Pending;
        
        AddEvent(new OrderCreatedEvent(Id, customerId));
    }

    public Guid CustomerId { get; private set; }
    public OrderStatus Status { get; private set; }
    public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
    public decimal TotalAmount => _items.Sum(i => i.TotalPrice);

    public void AddItem(Guid productId, int quantity, decimal unitPrice)
    {
        var item = new OrderItem(productId, quantity, unitPrice);
        _items.Add(item);
        
        AddEvent(new OrderItemAddedEvent(Id, productId, quantity, unitPrice));
    }

    public void Complete(string updatedBy)
    {
        if (Status != OrderStatus.Pending)
            throw new InvalidOperationException("Only pending orders can be completed");

        Status = OrderStatus.Completed;
        SetUpdatedBy(updatedBy);
        
        AddEvent(new OrderCompletedEvent(Id, CustomerId, TotalAmount, Items.Count));
    }

    public void Cancel(string reason, string updatedBy)
    {
        if (Status == OrderStatus.Completed)
            throw new InvalidOperationException("Completed orders cannot be cancelled");

        Status = OrderStatus.Cancelled;
        SetUpdatedBy(updatedBy);
        
        AddEvent(new OrderCancelledEvent(Id, reason));
    }
}

// Domain events
public record OrderCreatedEvent(Guid OrderId, Guid CustomerId);
public record OrderItemAddedEvent(Guid OrderId, Guid ProductId, int Quantity, decimal UnitPrice);
public record OrderCompletedEvent(Guid OrderId, Guid CustomerId, decimal TotalAmount, int ItemCount);
public record OrderCancelledEvent(Guid OrderId, string Reason);

Event Handler with Side Effects

public class OrderCompletedHandler : INotificationHandler<OrderCompletedEvent>
{
    private readonly IInventoryService _inventoryService;
    private readonly IPaymentService _paymentService;
    private readonly INotificationService _notificationService;
    private readonly ILogger<OrderCompletedHandler> _logger;

    public OrderCompletedHandler(
        IInventoryService inventoryService,
        IPaymentService paymentService,
        INotificationService notificationService,
        ILogger<OrderCompletedHandler> logger)
    {
        _inventoryService = inventoryService;
        _paymentService = paymentService;
        _notificationService = notificationService;
        _logger = logger;
    }

    public async Task Handle(OrderCompletedEvent notification, CancellationToken cancellationToken)
    {
        try
        {
            // Update inventory
            await _inventoryService.ReserveItemsAsync(notification.OrderId, cancellationToken);
            
            // Process payment
            await _paymentService.ProcessPaymentAsync(notification.OrderId, notification.TotalAmount, cancellationToken);
            
            // Send confirmation
            await _notificationService.SendOrderConfirmationAsync(notification.CustomerId, notification.OrderId, cancellationToken);
            
            _logger.LogInformation("Order {OrderId} completed successfully. Total: ${TotalAmount}, Items: {ItemCount}",
                notification.OrderId, notification.TotalAmount, notification.ItemCount);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to process order completion for {OrderId}", notification.OrderId);
            
            // Could add compensating actions or raise error events
            throw new EventException($"Failed to process order completion for {notification.OrderId}", ex);
        }
    }
}

Event Lifecycle

  1. Event Creation: Domain events are added to entities during business operations
  2. Event Queuing: Events are stored in entity's event collection until SaveChanges
  3. Event Publishing: Events are automatically published during EF Core SaveChanges via EventHook
  4. Event Handling: Registered event handlers process the events asynchronously
  5. Event Cleanup: Successfully processed events are cleared from entities

Error Handling

public class RobustEventPublisher : IEventPublisher
{
    private readonly IMediator _mediator;
    private readonly ILogger<RobustEventPublisher> _logger;

    public async Task PublishAsync(object eventItem, CancellationToken cancellationToken = default)
    {
        var maxRetries = 3;
        var retryDelay = TimeSpan.FromMilliseconds(100);

        for (int attempt = 1; attempt <= maxRetries; attempt++)
        {
            try
            {
                await _mediator.Publish((INotification)eventItem, cancellationToken);
                return;
            }
            catch (Exception ex) when (attempt < maxRetries)
            {
                _logger.LogWarning(ex, "Event publishing failed on attempt {Attempt} for {EventType}. Retrying...", 
                    attempt, eventItem.GetType().Name);
                    
                await Task.Delay(retryDelay * attempt, cancellationToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Event publishing failed after {MaxRetries} attempts for {EventType}", 
                    maxRetries, eventItem.GetType().Name);
                throw new EventException($"Failed to publish event after {maxRetries} attempts", ex);
            }
        }
    }
}

Performance Considerations

  • Batch Processing: Events are published in batches during SaveChanges
  • Async Handlers: All event handlers should be async for non-blocking execution
  • Memory Management: Events are cleared after successful publishing
  • Transaction Scope: Events are published within the same transaction as data changes

Best Practices

  • Single Responsibility: Keep event handlers focused on one concern
  • Idempotency: Design event handlers to be idempotent
  • Error Isolation: Don't let event handler failures affect the main transaction
  • Event Versioning: Plan for event schema evolution
  • Testing: Test event handlers independently from entities

Contributing

See the main CONTRIBUTING.md for guidelines on how to contribute to this project.

License

This project is licensed under the MIT License.


Part of the DKNet Framework - A comprehensive .NET framework for building modern, scalable applications.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on DKNet.EfCore.Events:

Package Downloads
DKNet.SlimBus.Extensions

DKNet is an enterprise-grade .NET library collection focused on advanced EF Core extensions, dynamic predicate building, and the Specification pattern. It provides production-ready tools for building robust, type-safe, and testable data access layers, including dynamic LINQ support, LinqKit integration. Designed for modern cloud-native applications, DKNet enforces strict code quality, async best practices, and full documentation for all public APIs. Enterprise-grade .NET library suite for modern application development, featuring advanced EF Core extensions (dynamic predicates, specifications, LinqKit), robust Domain-Driven Design (DDD) patterns, and domain event support. DKNet empowers scalable, maintainable, and testable solutions with type-safe validation, async/await, XML documentation, and high code quality standards. Ideal for cloud-native, microservices, and enterprise architectures.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.1.10 51 8/22/2026
10.1.9 63 8/22/2026
10.1.8 48 8/21/2026
10.1.7 65 8/21/2026
10.1.6 56 8/21/2026
10.1.5 76 8/20/2026
10.1.4 63 8/20/2026
10.1.3 73 8/19/2026
10.1.2 69 8/19/2026
10.1.1 59 8/19/2026
10.0.36 90 8/18/2026
10.0.35 100 8/5/2026
10.0.34 98 8/5/2026
10.0.33 107 8/4/2026
10.0.32 342 8/3/2026
10.0.31 129 7/21/2026
10.0.30 124 7/21/2026
10.0.29 679 6/22/2026
10.0.27 194 5/22/2026
10.0.26 123 5/19/2026
Loading failed