Crucible.Common.EntityEvents
0.2.0
dotnet add package Crucible.Common.EntityEvents --version 0.2.0
NuGet\Install-Package Crucible.Common.EntityEvents -Version 0.2.0
<PackageReference Include="Crucible.Common.EntityEvents" Version="0.2.0" />
<PackageVersion Include="Crucible.Common.EntityEvents" Version="0.2.0" />
<PackageReference Include="Crucible.Common.EntityEvents" />
paket add Crucible.Common.EntityEvents --version 0.2.0
#r "nuget: Crucible.Common.EntityEvents, 0.2.0"
#:package Crucible.Common.EntityEvents@0.2.0
#addin nuget:?package=Crucible.Common.EntityEvents&version=0.2.0
#tool nuget:?package=Crucible.Common.EntityEvents&version=0.2.0
Crucible.Common.EntityEvents
Entity event infrastructure for Entity Framework Core. Automatically generates events when entities are created, updated, or deleted.
Features
- Automatic event generation - Events are created from EF Core change tracking
- Transaction-aware - Events are generated after transaction commit to ensure consistency
- Framework-agnostic - No dependency on specific event publishing libraries (MediatR, etc.)
- Source generator included - Automatically add interfaces like
INotificationvia attribute - Simple registration - One method call configures everything
Installation
dotnet add package Crucible.Common.EntityEvents
Quick Start
1. Create Your DbContext
Inherit from EventPublishingDbContext and implement the PublishEventsAsync method:
using Crucible.Common.EntityEvents.Abstractions;
using Microsoft.Extensions.Logging;
[GenerateEntityEventInterfaces(typeof(INotification))]
public class MyContext : EventPublishingDbContext
{
public DbSet<MyEntity> MyEntities { get; set; }
public MyContext(DbContextOptions<MyContext> options) : base(options) { }
public override async Task PublishEventsAsync(IReadOnlyList<IEntityEvent> events, CancellationToken cancellationToken)
{
if (ServiceProvider is not null)
{
var mediator = ServiceProvider.GetRequiredService<IMediator>();
var logger = ServiceProvider.GetRequiredService<ILogger<MyContext>>();
foreach (var evt in events.Cast<INotification>())
{
try
{
await mediator.Publish(evt, cancellationToken);
}
catch (Exception ex)
{
logger.LogError(ex, "Error publishing entity event {EventType}", evt.GetType().Name);
}
}
}
}
}
The EntityEventInterceptor handles calling PublishEventsAsync at the right time (after SaveChanges for non-transactional operations, or after transaction commit). Events are passed as a parameter — the interceptor creates the events and clears its tracked state before calling your method. You can use the publishing mechanism of your choice. The example above uses MediatR.
2. Register Services
Use the AddEventPublishingDbContextFactory extension method for simple registration:
using Crucible.Common.EntityEvents.Extensions;
services.AddEventPublishingDbContextFactory<MyContext>((sp, builder) =>
builder.UseNpgsql(connectionString));
This single call:
- Registers the
EntityEventInterceptor - Creates a pooled DbContext factory with the interceptor configured
- Registers a scoped
MyContextwithServiceProviderinjected
Alternative: Manual Registration
If you need more control over registration:
services.AddEntityEventInterceptor();
services.AddPooledDbContextFactory<MyContext>((sp, builder) => builder
.AddInterceptors(sp.GetRequiredService<EntityEventInterceptor>())
.UseNpgsql(connectionString));
// Register scoped context with ServiceProvider injection
services.AddScoped(sp =>
{
var factory = sp.GetRequiredService<IDbContextFactory<MyContext>>();
var context = factory.CreateDbContext();
context.ServiceProvider = sp;
return context;
});
Publishing Integration
Publishing libraries may require specific interfaces be present on their published classes. To support this without tying this library to any specific publishing mechanism, all IEntityEvent classes are partial, allowing for them to be extended in the consuming application with any required interfaces.
Automatic (Recommended) - Source Generator
Add the GenerateEntityEventInterfaces attribute directly on your DbContext class:
using Crucible.Common.EntityEvents;
using MediatR;
[GenerateEntityEventInterfaces(typeof(INotification))]
public class MyContext : EventPublishingDbContext
{
// ...
}
The source generator automatically creates partial class declarations that implement the passed in types for all entity event types. You can pass multiple types:
[GenerateEntityEventInterfaces(typeof(INotification), typeof(IMyCustomInterface))]
Manual (Legacy)
If the source generator doesn't work in your environment, create partial class extensions manually:
using Crucible.Common.EntityEvents.Events;
using MediatR;
namespace Crucible.Common.EntityEvents.Events;
public partial class EntityCreated<TEntity> : INotification { }
public partial class EntityUpdated<TEntity> : INotification { }
public partial class EntityDeleted<TEntity> : INotification { }
Event Types
- EntityCreated<TEntity> - Raised when an entity is added
- EntityUpdated<TEntity> - Raised when an entity is modified (includes
ModifiedProperties) - EntityDeleted<TEntity> - Raised when an entity is deleted
How It Works
EntityEventInterceptorinterceptsSavingChangesto capture entity states- After transaction commits (or SaveChanges completes if no transaction), events are created
- The interceptor calls your
PublishEventsAsyncimplementation to publish the events - The interceptor automatically clears events and tracked state after publishing
- If a transaction is rolled back, tracked state is cleared without publishing
This ensures events are only published for changes that actually persisted to the database.
Advanced: Manual IEventPublishingDbContext Implementation
If you need full control, implement IEventPublishingDbContext directly:
using Crucible.Common.EntityEvents.Abstractions;
public class MyContext : DbContext, IEventPublishingDbContext
{
public IServiceProvider? ServiceProvider { get; set; }
public List<TrackedEntityEntry> TrackedEntries { get; } = [];
public Task PublishEventsAsync(IReadOnlyList<IEntityEvent> events, CancellationToken ct = default)
{
// Your event publishing logic here.
// The interceptor creates events and passes them to this method.
return Task.CompletedTask;
}
}
Note:
TrackedEntriesis used internally by the interceptor to track entity changes across multipleSaveChangescalls within a transaction. It must be stored on the DbContext (not the interceptor) for thread safety when using pooled contexts.
| 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
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.