🧠 Auditing and Domain Event Publishing with EF Core
In this post, we will explore how to use Entity Framework Core to implement two fundamental patterns in enterprise applications: entity auditing and domain event publishing.
🔍 Automatic Entity Auditing
EF Core allows us to implement an automatic auditing system by overriding the SaveChangesAsync method:
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default){ foreach (var entry in ChangeTracker.Entries<AuditableEntity>()) { switch (entry.State) { case EntityState.Added: entry.Entity.CreatedBy = _currentUserService.UserId; entry.Entity.Created = _dateTime.Now; break; case EntityState.Modified: entry.Entity.LastModifiedBy = _currentUserService.UserId; entry.Entity.LastModified = _dateTime.Now; break; } } // Rest of the implementation...}🎯 Key Benefits
- 🛠 Complete automation of audit fields
- 🔄 Guaranteed consistency across all changes
📌 Centralizing Logic in a Single Place
Requirement: Entities must implement AuditableEntity with properties such as:
- CreatedBy (who created the record)
- Created (when it was created)
- LastModifiedBy (last editor)
- LastModified (last modification)
🌐 Domain Events with EF Core
The same context demonstrates an efficient pattern for handling domain events:
var events = ChangeTracker.Entries<IHasDomainEvent>() .Select(x => x.Entity.DomainEvents) .SelectMany(x => x) .Where(domainEvent => !domainEvent.IsPublished) .ToArray();
var result = await base.SaveChangesAsync(cancellationToken);
await DispatchEvents(events);⚡ Main Features
- Safe workflow:
- ✅ Changes are first committed to the database
- ✅ Then events are published
- Duplicate prevention mechanism:
- 🔍 Filters only unpublished events (!domainEvent.IsPublished)
- 🏷 Marks events as published before sending them
- Clean integration:
- 🧩 Uses an IHasDomainEvent interface to identify entities with events
- 📤 Delegates publishing to a specialized service (IDomainEventService)
⚙️ Additional Configuration
The context also includes valuable configurations:
protected override void OnModelCreating(ModelBuilder builder){ // Ignore DomainEvent in the model builder.Ignore<DomainEvent>(); builder.Ignore<List<DomainEvent>>();
// Configure DateTime to use UTC foreach (var entityType in builder.Model.GetEntityTypes()) { foreach (var property in entityType.GetProperties()) { if (property.ClrType == typeof(DateTime) || property.ClrType == typeof(DateTime?)) { property.SetValueConverter(new ValueConverter<DateTime, DateTime>( v => v.Kind == DateTimeKind.Utc ? v : DateTime.SpecifyKind(v, DateTimeKind.Utc), v => DateTime.SpecifyKind(v, DateTimeKind.Utc))); } } }}🏆 Conclusion
This approach offers us:
- 📊 Robust and automatic auditing
- 🚀 Reliable domain events
- ⏱ Professional UTC date handling
- 💎 Elegant integration into the DbContext