๐ง 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