A base class and utilities to implement auditing with SQL Server temporal tables.
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Design
burtonrodman.EntityFrameworkCore.Auditing
ICurrentUserAccessor
interface and returns the current user's identifier (whatever that means in your domain). This will often be the Name property of the current ClaimsPrincipal, but may vary in your domain.
public class CurrentUserAccessor : ICurrentUserAccessor
{
public string GetUserName() => "current.user@domain.com";
}
services.AddScoped<ICurrentUserAccessor, CurrentUserAccessor>();
Alternatively you may provide an instance of DelegateCurrentUserAccessor
that takes a Func<string>
, or Func<IServiceProvider, string>
that retrieves the user name.
services.AddScoped<ICurrentUserAccessor>(new DelegateCurrentUserAccessor(() => "current.user@domain.com"));
OR
services.AddScoped<ICurrentUserAccessor>(serviceProvider => new DelegateCurrentUserAccessor(sp => {
var context = sp.GetRequiredService<IHttpContextAccessor>();
return context.HttpContext.Identity?.Name;
}, serviceProvider));
burtonrodman.EntityFrameworkCore.Auditing
to your DbContext file or global usings.Usings.cs:
global using burtonrodman.EntityFrameworkCore.Auditing;
AuditableDbContext
and add or modify the constructor to take the instance of ICurrentUserAccessor
and pass it to the base constructor.
public class SampleDbContext : AuditableDbContext
{
public SampleDbContext(
DbContextOptions<SampleDbContext> dbContextOptions,
ICurrentUserAccessor currentUserAccessor)
: base(dbContextOptions, currentUserAccessor)
{ }
AuditableEntityBase
.
public class BlogPost : AuditableEntityBase
for testing scenarios where you may be using an in-memory or sqlite provider, the shadow properties provided by the SQL Server provider will not be available. We have the shouldAddShadowProperties
parameter that adds them so your queries using the PeriodStart and PeriodEnd columns do not break during testing.
override OnModelCreating:
override protected void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(SampleDbContext).Assembly);
ConfigureTemporalTables(modelBuilder,
shouldAddShadowProperties: !this.Database.IsSqlServer());
}
public SampleDbContext(
DbContextOptions<SampleDbContext> dbContextOptions,
ICurrentUserAccessor currentUserAccessor)
: base(dbContextOptions, currentUserAccessor)
{
// OPTIONAL: override the default Period column names
this.PeriodStart = "SysStartTime";
this.PeriodEnd = "SysEndTime";
}
Now that your DbContext and entities are configured, you may generate a new migration that will apply System-versioning and also add the ModifiedBy column to your table(s).
dotnet ef migrations add AddAuditing
dotnet ef database update
SQL Server has a feature called "Temporal Tables" or "System-versioned Tables". When enabled on a table, 2 columns are added to the existing table -- their names may vary, but I have chosen PeriodStart and PeriodEnd. In addition, another table with the same schema and the suffix History
is created. From then on, any insert the PeriodStart is populated with the current server time; any update inserts a new row with PeriodStart as the current server time and moves the old version of the row to the History table, with PeriodEnd also set; any delete removes the row from the current table and inserts into the History
table with the PeriodEnd set.
In order to create a full audit log (including when AND who), this library adds the ModifiedBy field. SaveChanges[Async]
is overridden and updates the ModifiedBy column. For deleted rows, the state is changed to Modified and the ModifiedBy field is updated before finally deleting the row. This gives full auditing of inserts, updates and deletes.
ConfigureTemporalTables
public partial class MyDbContext : AuditableDbContext
{
// provide static values for PeriodStart and PeriodEnd for mapping Profile
public const string PeriodStartColumnName = "SysStartTime";
public const string PeriodEndColumnName = "SysEndTime";
Use in constructor:
// OPTIONAL: override the default Period column names
this.PeriodStart = PeriodStartColumnName;
this.PeriodEnd = PeriodEndColumnName;
I welcome Pull Requests for any improvement or bug fixes. Please open an Issue for discussion if you plan on adding any features, so that we can collaborate on design. For bug reports, a Pull Request with a failing unit test is ideal.
Thanks!