DKNet.EfCore.Events
10.1.1
See the version list below for details.
dotnet add package DKNet.EfCore.Events --version 10.1.1
NuGet\Install-Package DKNet.EfCore.Events -Version 10.1.1
<PackageReference Include="DKNet.EfCore.Events" Version="10.1.1" />
<PackageVersion Include="DKNet.EfCore.Events" Version="10.1.1" />
<PackageReference Include="DKNet.EfCore.Events" />
paket add DKNet.EfCore.Events --version 10.1.1
#r "nuget: DKNet.EfCore.Events, 10.1.1"
#:package DKNet.EfCore.Events@10.1.1
#addin nuget:?package=DKNet.EfCore.Events&version=10.1.1
#tool nuget:?package=DKNet.EfCore.Events&version=10.1.1
DKNet.EfCore.Events
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 inspectEntityEntry.Property(...).IsModified) and published through the sameIEventPublisherpath as hand-raised events, after a successful save. - Coexistence: an entity can both declare events via
[RaisesEvent]and raise events by hand viaAddEvent(...)— both are published, as distinct types, from the same save. - No base-class requirement: any entity mapped by the
DbContextmay declare events; it does not need to be anAggregateRootor implementIEventEntity. - 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 abstractionIEventEntity- 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 entityClearEvents()- Clear all queued eventsGetEvents()- 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
- Event Creation: Domain events are added to entities during business operations
- Event Queuing: Events are stored in entity's event collection until SaveChanges
- Event Publishing: Events are automatically published during EF Core SaveChanges via EventHook
- Event Handling: Registered event handlers process the events asynchronously
- 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.
Related Packages
- DKNet.EfCore.Abstractions - Core abstractions including IEventEntity
- DKNet.EfCore.Extensions - EF Core functionality extensions
- DKNet.EfCore.Hooks - EF Core lifecycle hooks (used internally)
- DKNet.SlimBus.Extensions - Alternative CQRS event handling
Part of the DKNet Framework - A comprehensive .NET framework for building modern, scalable applications.
| Product | Versions 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. |
-
net10.0
- DKNet.EfCore.Abstractions (>= 10.1.1)
- DKNet.EfCore.Hooks (>= 10.1.1)
- FluentResults (>= 4.0.0)
- Mapster (>= 10.0.11)
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 | 47 | 8/22/2026 |
| 10.1.9 | 56 | 8/22/2026 |
| 10.1.8 | 46 | 8/21/2026 |
| 10.1.7 | 60 | 8/21/2026 |
| 10.1.6 | 54 | 8/21/2026 |
| 10.1.5 | 75 | 8/20/2026 |
| 10.1.4 | 63 | 8/20/2026 |
| 10.1.3 | 67 | 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 |