01 Sep 2026
The Unit of Work pattern ensures that a group of operations are treated as a single transaction. In EF Core, the DbContext itself is the Unit of Work. Every handler calls SaveChangesAsync() as the single commit point. But Brand Website V3 takes this a step further: every module's DbContext overrides SaveChangesAsync to inject automatic audit fields. No handler manually sets CreatedBy or LastModifiedAt. It just happens.
// Blog.Database/BlogDbContext.cs
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new())
{
foreach (var entry in ChangeTracker.Entries<AuditableEntity>())
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedBy ??= "SYSTEM";
entry.Entity.CreatedAt = DateTime.Now;
break;
case EntityState.Modified:
entry.Entity.LastModifiedBy ??= "SYSTEM";
entry.Entity.LastModifiedAt = DateTime.Now;
break;
}
}
return await base.SaveChangesAsync(cancellationToken);
}
When a handler calls SaveChangesAsync, the DbContext walks through every tracked entity. If it is newly added, the created audit fields are set. If it is modified, the last-modified audit fields are set. Then base.SaveChangesAsync commits everything in a single transaction. The handler does not need to know about auditing. The auditing just works.
The AuditableEntity base class (from AugusteVN.Database.Entities) provides the audit properties out of the box:
public abstract class AuditableEntity
{
public string CreatedBy { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public string? LastModifiedBy { get; set; }
public DateTime? LastModifiedAt { get; set; }
}
Every entity in every module inherits from this base class. The SaveChangesAsync override in each module's DbContext handles the rest. It is consistent, automatic, and impossible to forget.
SaveChangesAsync call, one transaction. Either all changes commit or none do.await _dbContext.SaveChangesAsync(cancellationToken);
DbContext enforces them on every save. No handler can skip setting CreatedAt or CreatedBy.entry.Entity.CreatedBy ??= "SYSTEM";
entry.Entity.CreatedAt = DateTime.Now;
return await base.SaveChangesAsync(cancellationToken);
SaveChangesAsync. That is it. No audit plumbing, no transaction management._dbContext.BlogPosts.Update(existingBlogPost);
await _dbContext.SaveChangesAsync(cancellationToken);
This pattern is not unique to the Blog module. Every single module in Brand Website V3 has its own DbContext with the same SaveChangesAsync override. It is a convention that is followed everywhere, which means auditing is guaranteed across the entire application.
Brand Website V3 implements the Unit of Work pattern with automatic auditing across all 11 domain modules. Every SaveChangesAsync call is atomic and audit-safe. See the implementation at kiss-code.com.
I continuously build, learn and experiment with innovative technology. Allow me to share what I learn, with you.