From b33ebec68f17fd5f9518850b4895128b30500056 Mon Sep 17 00:00:00 2001 From: JJ Bussert Date: Thu, 24 Apr 2025 11:21:14 -0500 Subject: [PATCH] Refactor DbContext and related interfaces to be generic - Made `BaseDbContext` and `IAuditContext` generic to support type parameter `T` for `AuditUser`. - Updated interceptors (`CreatableInterceptor`, `ModifiableInterceptor`, `SoftDeleteInterceptor`) to work with `IAuditContext`. - Refactored `ICreatable`, `IModifiable`, `IDeletable`, `IEffectable`, and `IExpirable` interfaces to be generic. - Adjusted test classes and methods to align with the new generic structure. - Removed `SaveChanges_NoTagEntries` method; integrated its functionality into `SaveChanges`. - Modified `TestDbContext` and related tests to utilize the new generic interfaces. --- src/E13.Common.Data.Db/BaseDbContext.cs | 38 ++----------------- src/E13.Common.Data.Db/IAuditContext.cs | 4 +- .../Interceptors/CreatableInterceptor.cs | 10 ++--- .../Interceptors/ModifiableInterceptor.cs | 10 ++--- .../Interceptors/SoftDeleteInterceptor.cs | 13 +++---- src/E13.Common.Domain/ICreatable.cs | 4 +- src/E13.Common.Domain/IDeletable.cs | 4 +- src/E13.Common.Domain/IEffectable.cs | 4 +- src/E13.Common.Domain/IExpirable.cs | 8 ++-- src/E13.Common.Domain/IModifiable.cs | 4 +- src/E13.Common.Domain/IOwnable.cs | 4 +- .../BaseDbContext_ICreatableTests.cs | 18 ++++----- .../BaseDbContext_IDeletableTests.cs | 6 +-- .../BaseDbContext_IModifiableTests.cs | 8 ++-- .../Sample/TestCreatable.cs | 2 +- .../Sample/TestDbContext.cs | 7 +++- .../Sample/TestDeletable.cs | 2 +- .../Sample/TestEffectable.cs | 2 +- .../Sample/TestInvalidDeletable.cs | 2 +- .../Sample/TestModifiable.cs | 2 +- .../Sample/TestOwnable.cs | 6 +-- .../IDeletableTests.cs | 4 +- 22 files changed, 66 insertions(+), 96 deletions(-) diff --git a/src/E13.Common.Data.Db/BaseDbContext.cs b/src/E13.Common.Data.Db/BaseDbContext.cs index 135843d..ee3d090 100644 --- a/src/E13.Common.Data.Db/BaseDbContext.cs +++ b/src/E13.Common.Data.Db/BaseDbContext.cs @@ -16,7 +16,7 @@ namespace E13.Common.Data.Db { - public abstract class BaseDbContext : DbContext, IAuditContext + public abstract class BaseDbContext : DbContext, IAuditContext { /// /// The user name used when the user name is null @@ -25,7 +25,7 @@ public abstract class BaseDbContext : DbContext, IAuditContext protected ILogger Logger { get;} - public string? AuditUser { get; set; } + public required T AuditUser { get; set; } public string? Source { get; set; } @@ -42,41 +42,9 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.HasQueryFiltersFor(e => e.Deleted != null); + modelBuilder.HasQueryFiltersFor>(e => e.Deleted != null); base.OnModelCreating(modelBuilder); } - - /// - /// This is used by E13.Common.Data.Db.Tests in order to allow TestDbContext.TestSeed() to seed - /// data without going through the TagEntries. - /// - /// This is necessary so that the unit tests can, via code, initialize the context with data that - /// may normally be prevented by TagChanges(). An example being a scenario where a backend database - /// is manually adjusted via raw SQL and puts entities into an invalid state. - /// - /// The tests will be ensuring that subsequent calls automatically clean up bad entity states caused - /// by manual data manipulation. - /// - /// - internal int SaveChanges_NoTagEntries() - { - return base.SaveChanges(); - } - - public override int SaveChanges() - { - // 1 – Get caller outside EF - var caller = new StackFrame(1).GetMethod(); - var source = $"{caller?.DeclaringType?.FullName}.{caller?.Name}"; - - // 2 – Stash it in the scoped audit context (injected) - Source = source ?? "Unknown"; - AuditUser = AuditUser ?? UnknownUser; - - // 3 – Proceed. Interceptors now have Source & User. - return base.SaveChanges(); - } - } } diff --git a/src/E13.Common.Data.Db/IAuditContext.cs b/src/E13.Common.Data.Db/IAuditContext.cs index e5165a7..607d55e 100644 --- a/src/E13.Common.Data.Db/IAuditContext.cs +++ b/src/E13.Common.Data.Db/IAuditContext.cs @@ -6,9 +6,9 @@ namespace E13.Common.Data.Db { - public interface IAuditContext + public interface IAuditContext { - string? AuditUser { get; } + T AuditUser { get; } string? Source { get; } } } diff --git a/src/E13.Common.Data.Db/Interceptors/CreatableInterceptor.cs b/src/E13.Common.Data.Db/Interceptors/CreatableInterceptor.cs index fe65308..4d47bf3 100644 --- a/src/E13.Common.Data.Db/Interceptors/CreatableInterceptor.cs +++ b/src/E13.Common.Data.Db/Interceptors/CreatableInterceptor.cs @@ -10,13 +10,13 @@ namespace E13.Common.Data.Db.Interceptors { - public sealed class CreatableInterceptor : SaveChangesInterceptor + public sealed class CreatableInterceptor : SaveChangesInterceptor { public override InterceptionResult SavingChanges( DbContextEventData eventData, InterceptionResult result) { - var auditContext = eventData.Context as IAuditContext ?? throw new Exception("Audit context is not set."); + var auditContext = eventData.Context as IAuditContext ?? throw new Exception("Audit context is not set."); HandleEventData(eventData, auditContext); @@ -28,7 +28,7 @@ public override ValueTask> SavingChangesAsync( InterceptionResult result, CancellationToken cancellationToken = default) { - var auditContext = eventData.Context as IAuditContext ?? throw new Exception("Audit context is not set."); + var auditContext = eventData.Context as IAuditContext ?? throw new Exception("Audit context is not set."); HandleEventData(eventData, auditContext); @@ -40,9 +40,9 @@ public override ValueTask> SavingChangesAsync( /// /// /// - private static void HandleEventData(DbContextEventData eventData, IAuditContext auditContext) + private static void HandleEventData(DbContextEventData eventData, IAuditContext auditContext) { - foreach (var entry in eventData.Context!.ChangeTracker.Entries() + foreach (var entry in eventData.Context!.ChangeTracker.Entries>() .Where(e => e.State == EntityState.Added)) { entry.Entity.Created = DateTime.UtcNow; diff --git a/src/E13.Common.Data.Db/Interceptors/ModifiableInterceptor.cs b/src/E13.Common.Data.Db/Interceptors/ModifiableInterceptor.cs index 176926e..26951f0 100644 --- a/src/E13.Common.Data.Db/Interceptors/ModifiableInterceptor.cs +++ b/src/E13.Common.Data.Db/Interceptors/ModifiableInterceptor.cs @@ -11,13 +11,13 @@ namespace E13.Common.Data.Db.Interceptors { - public sealed class ModifiableInterceptor : SaveChangesInterceptor + public sealed class ModifiableInterceptor : SaveChangesInterceptor { public override InterceptionResult SavingChanges( DbContextEventData eventData, InterceptionResult result) { - var auditContext = eventData.Context as IAuditContext ?? throw new Exception("Audit context is not set."); + var auditContext = eventData.Context as IAuditContext ?? throw new Exception("Audit context is not set."); HandleEventData(eventData, auditContext); @@ -29,7 +29,7 @@ public override ValueTask> SavingChangesAsync( InterceptionResult result, CancellationToken cancellationToken = default) { - var auditContext = eventData.Context as IAuditContext ?? throw new Exception("Audit context is not set."); + var auditContext = eventData.Context as IAuditContext ?? throw new Exception("Audit context is not set."); HandleEventData(eventData, auditContext); @@ -41,9 +41,9 @@ public override ValueTask> SavingChangesAsync( /// /// /// - private static void HandleEventData(DbContextEventData eventData, IAuditContext auditContext) + private static void HandleEventData(DbContextEventData eventData, IAuditContext auditContext) { - foreach (var entry in eventData.Context!.ChangeTracker.Entries() + foreach (var entry in eventData.Context!.ChangeTracker.Entries>() .Where(e => e.State is EntityState.Added or EntityState.Modified)) { entry.Entity.Modified = DateTime.UtcNow; diff --git a/src/E13.Common.Data.Db/Interceptors/SoftDeleteInterceptor.cs b/src/E13.Common.Data.Db/Interceptors/SoftDeleteInterceptor.cs index 64262dd..54b8a2f 100644 --- a/src/E13.Common.Data.Db/Interceptors/SoftDeleteInterceptor.cs +++ b/src/E13.Common.Data.Db/Interceptors/SoftDeleteInterceptor.cs @@ -7,17 +7,16 @@ using System.Text; using System.Threading.Tasks; using System.Threading; -using static System.Runtime.InteropServices.JavaScript.JSType; namespace E13.Common.Data.Db.Interceptors { - public sealed class SoftDeleteInterceptor : SaveChangesInterceptor + public sealed class SoftDeleteInterceptor : SaveChangesInterceptor { public override InterceptionResult SavingChanges( DbContextEventData eventData, InterceptionResult result) { - var auditContext = eventData.Context as IAuditContext ?? throw new Exception("Audit context is not set."); + var auditContext = eventData.Context as IAuditContext ?? throw new Exception("Audit context is not set."); HandleEventData(eventData, auditContext); @@ -29,7 +28,7 @@ public override ValueTask> SavingChangesAsync( InterceptionResult result, CancellationToken cancellationToken = default) { - var auditContext = eventData.Context as IAuditContext ?? throw new Exception("Audit context is not set."); + var auditContext = eventData.Context as IAuditContext ?? throw new Exception("Audit context is not set."); HandleEventData(eventData, auditContext); @@ -41,9 +40,9 @@ public override ValueTask> SavingChangesAsync( /// /// /// - private static void HandleEventData(DbContextEventData eventData, IAuditContext auditContext) + private static void HandleEventData(DbContextEventData eventData, IAuditContext auditContext) { - foreach (var entry in eventData.Context!.ChangeTracker.Entries()) + foreach (var entry in eventData.Context!.ChangeTracker.Entries>()) { if (entry.State == EntityState.Deleted) { @@ -59,7 +58,7 @@ private static void HandleEventData(DbContextEventData eventData, IAuditContext { // “undelete” scenario entry.Entity.Deleted = null; - entry.Entity.DeletedBy = null; + entry.Entity.DeletedBy = default(T); entry.Entity.DeletedSource = null; } } diff --git a/src/E13.Common.Domain/ICreatable.cs b/src/E13.Common.Domain/ICreatable.cs index 186466f..a5c660c 100644 --- a/src/E13.Common.Domain/ICreatable.cs +++ b/src/E13.Common.Domain/ICreatable.cs @@ -6,9 +6,9 @@ namespace E13.Common.Domain { - public interface ICreatable : IEntity + public interface ICreatable : IEntity { - string? CreatedBy { get; set; } + T? CreatedBy { get; set; } string? CreatedSource { get; set; } DateTime? Created { get; set; } } diff --git a/src/E13.Common.Domain/IDeletable.cs b/src/E13.Common.Domain/IDeletable.cs index d6a3b3c..05dfd76 100644 --- a/src/E13.Common.Domain/IDeletable.cs +++ b/src/E13.Common.Domain/IDeletable.cs @@ -4,9 +4,9 @@ namespace E13.Common.Domain { - public interface IDeletable : IEntity + public interface IDeletable : IEntity { - string? DeletedBy { get; set; } + T DeletedBy { get; set; } string? DeletedSource { get; set; } DateTime? Deleted { get; set; } diff --git a/src/E13.Common.Domain/IEffectable.cs b/src/E13.Common.Domain/IEffectable.cs index d7fad36..25a28d1 100644 --- a/src/E13.Common.Domain/IEffectable.cs +++ b/src/E13.Common.Domain/IEffectable.cs @@ -4,9 +4,9 @@ namespace E13.Common.Domain { - public interface IEffectable : IEntity + public interface IEffectable : IEntity { - string? EffectiveBy { get; set; } + T EffectiveBy { get; set; } string? EffectiveSource { get; set; } DateTime? Effective { get; set; } } diff --git a/src/E13.Common.Domain/IExpirable.cs b/src/E13.Common.Domain/IExpirable.cs index 1f3d807..909181d 100644 --- a/src/E13.Common.Domain/IExpirable.cs +++ b/src/E13.Common.Domain/IExpirable.cs @@ -4,10 +4,10 @@ namespace E13.Common.Domain { - public interface IExpirable : IEntity + public interface IExpirable : IEntity { - string ExpirationBy { get; set; } - string ExpirationSource { get; set; } - DateTime Expiration { get; set; } + T ExpirationBy { get; set; } + string? ExpirationSource { get; set; } + DateTime? Expiration { get; set; } } } diff --git a/src/E13.Common.Domain/IModifiable.cs b/src/E13.Common.Domain/IModifiable.cs index 5300508..1075ea2 100644 --- a/src/E13.Common.Domain/IModifiable.cs +++ b/src/E13.Common.Domain/IModifiable.cs @@ -6,9 +6,9 @@ namespace E13.Common.Domain { - public interface IModifiable : IEntity + public interface IModifiable : IEntity { - string? ModifiedBy { get; set; } + T ModifiedBy { get; set; } string? ModifiedSource { get; set; } DateTime? Modified { get; set; } } diff --git a/src/E13.Common.Domain/IOwnable.cs b/src/E13.Common.Domain/IOwnable.cs index 3790192..d7041bc 100644 --- a/src/E13.Common.Domain/IOwnable.cs +++ b/src/E13.Common.Domain/IOwnable.cs @@ -7,7 +7,7 @@ namespace E13.Common.Domain public interface IOwnable : IEntity { T OwnedBy { get; set; } - string OwnedSource { get; set; } - DateTime Owned { get; set; } + string? OwnedSource { get; set; } + DateTime? Owned { get; set; } } } diff --git a/test/E13.Common.Data.Db.Tests/BaseDbContext_ICreatableTests.cs b/test/E13.Common.Data.Db.Tests/BaseDbContext_ICreatableTests.cs index 7df70c8..7284217 100644 --- a/test/E13.Common.Data.Db.Tests/BaseDbContext_ICreatableTests.cs +++ b/test/E13.Common.Data.Db.Tests/BaseDbContext_ICreatableTests.cs @@ -17,17 +17,17 @@ public class BaseDbContext_ICreatableTests public void Setup() { var services = new ServiceCollection(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddDbContext((sp, o) => + services.AddScoped>(); + services.AddScoped>(); + services.AddScoped>(); + services.AddDbContext, TestDbContext>((sp, o) => { o.UseInMemoryDatabase($"{Guid.NewGuid()}"); o.EnableSensitiveDataLogging(); o.AddInterceptors( - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService()); + sp.GetRequiredService>(), + sp.GetRequiredService>(), + sp.GetRequiredService>()); }); Context = services.BuildServiceProvider().GetService(); @@ -56,7 +56,7 @@ public void InitialData_CreatedBy_Unknown() var arranged = Context.Creatables.First(); - arranged.CreatedBy.Should().Be(BaseDbContext.UnknownUser); + arranged.CreatedBy.Should().Be(BaseDbContext.UnknownUser); } [Test] @@ -71,7 +71,7 @@ public void SaveChanges_UnknownUser_CreatedByUnknown() var arranged = Context.Creatables.First(e => e.Id == id); - arranged.CreatedBy.Should().Be(BaseDbContext.UnknownUser); + arranged.CreatedBy.Should().Be(BaseDbContext.UnknownUser); } [Test] diff --git a/test/E13.Common.Data.Db.Tests/BaseDbContext_IDeletableTests.cs b/test/E13.Common.Data.Db.Tests/BaseDbContext_IDeletableTests.cs index 412f5fa..7a0b453 100644 --- a/test/E13.Common.Data.Db.Tests/BaseDbContext_IDeletableTests.cs +++ b/test/E13.Common.Data.Db.Tests/BaseDbContext_IDeletableTests.cs @@ -19,9 +19,9 @@ public void Setup() var services = new ServiceCollection(); services.AddDbContext(o => { o.UseInMemoryDatabase($"{Guid.NewGuid()}"); - o.AddInterceptors(new CreatableInterceptor()); - o.AddInterceptors(new ModifiableInterceptor()); - o.AddInterceptors(new SoftDeleteInterceptor()); + o.AddInterceptors(new CreatableInterceptor()); + o.AddInterceptors(new ModifiableInterceptor()); + o.AddInterceptors(new SoftDeleteInterceptor()); }); Context = services.BuildServiceProvider().GetService(); diff --git a/test/E13.Common.Data.Db.Tests/BaseDbContext_IModifiableTests.cs b/test/E13.Common.Data.Db.Tests/BaseDbContext_IModifiableTests.cs index 3fcf86e..095d9c6 100644 --- a/test/E13.Common.Data.Db.Tests/BaseDbContext_IModifiableTests.cs +++ b/test/E13.Common.Data.Db.Tests/BaseDbContext_IModifiableTests.cs @@ -19,9 +19,9 @@ public void Setup() var services = new ServiceCollection(); services.AddDbContext(o => { o.UseInMemoryDatabase($"{Guid.NewGuid()}"); - o.AddInterceptors(new CreatableInterceptor()); - o.AddInterceptors(new ModifiableInterceptor()); - o.AddInterceptors(new SoftDeleteInterceptor()); + o.AddInterceptors(new CreatableInterceptor()); + o.AddInterceptors(new ModifiableInterceptor()); + o.AddInterceptors(new SoftDeleteInterceptor()); }); Context = services.BuildServiceProvider().GetService(); @@ -79,7 +79,7 @@ public void SaveChanges_UnspecifiedUser_ModifiedByUnknown() Context.SaveChanges(); var assert = Context.Modifiables.First(); - assert.ModifiedBy.Should().Be(BaseDbContext.UnknownUser); + assert.ModifiedBy.Should().Be(BaseDbContext.UnknownUser); } [Test] diff --git a/test/E13.Common.Data.Db.Tests/Sample/TestCreatable.cs b/test/E13.Common.Data.Db.Tests/Sample/TestCreatable.cs index 3abc17d..0149193 100644 --- a/test/E13.Common.Data.Db.Tests/Sample/TestCreatable.cs +++ b/test/E13.Common.Data.Db.Tests/Sample/TestCreatable.cs @@ -7,7 +7,7 @@ namespace E13.Common.Data.Db.Tests.Sample { - public class TestCreatable : ICreatable + public class TestCreatable : ICreatable { public Guid Id { get; set; } public string Text { get; set; } = string.Empty; diff --git a/test/E13.Common.Data.Db.Tests/Sample/TestDbContext.cs b/test/E13.Common.Data.Db.Tests/Sample/TestDbContext.cs index 47c64a4..58e735c 100644 --- a/test/E13.Common.Data.Db.Tests/Sample/TestDbContext.cs +++ b/test/E13.Common.Data.Db.Tests/Sample/TestDbContext.cs @@ -8,7 +8,7 @@ namespace E13.Common.Data.Db.Tests.Sample { - public class TestDbContext : BaseDbContext + public class TestDbContext : BaseDbContext { public DbSet Creatables { get; set; } public DbSet Modifiables { get; set; } @@ -29,6 +29,9 @@ public TestDbContext(DbContextOptions opt) public void AddTestData() { + AuditUser = UnknownUser; + Source = "E13.Common.Data.Db.Tests.Sample.TestDbContext.AddTestData"; + Creatables.Add(new TestCreatable { Id = Guid.Empty @@ -66,7 +69,7 @@ public void AddTestData() DeletedSource = "PreviousSource" }); // Because invalid data is being arranged the internal SaveChanges avoiding the TagEntries code must be used. - SaveChanges_NoTagEntries(); + SaveChanges(); } } } diff --git a/test/E13.Common.Data.Db.Tests/Sample/TestDeletable.cs b/test/E13.Common.Data.Db.Tests/Sample/TestDeletable.cs index d6ed1ca..fed4335 100644 --- a/test/E13.Common.Data.Db.Tests/Sample/TestDeletable.cs +++ b/test/E13.Common.Data.Db.Tests/Sample/TestDeletable.cs @@ -7,7 +7,7 @@ namespace E13.Common.Data.Db.Tests.Sample { - public class TestDeletable : IDeletable + public class TestDeletable : IDeletable { public Guid Id { get; set; } public string? DeletedBy { get; set; } diff --git a/test/E13.Common.Data.Db.Tests/Sample/TestEffectable.cs b/test/E13.Common.Data.Db.Tests/Sample/TestEffectable.cs index 368b764..4b65272 100644 --- a/test/E13.Common.Data.Db.Tests/Sample/TestEffectable.cs +++ b/test/E13.Common.Data.Db.Tests/Sample/TestEffectable.cs @@ -7,7 +7,7 @@ namespace E13.Common.Data.Db.Tests.Sample { - public class TestEffectable : IEffectable + public class TestEffectable : IEffectable { public Guid Id { get; set; } public string? EffectiveBy { get; set; } = "EffectiveBy"; diff --git a/test/E13.Common.Data.Db.Tests/Sample/TestInvalidDeletable.cs b/test/E13.Common.Data.Db.Tests/Sample/TestInvalidDeletable.cs index d8ac92b..f222ee3 100644 --- a/test/E13.Common.Data.Db.Tests/Sample/TestInvalidDeletable.cs +++ b/test/E13.Common.Data.Db.Tests/Sample/TestInvalidDeletable.cs @@ -7,7 +7,7 @@ namespace E13.Common.Data.Db.Tests.Sample { - public class TestInvalidDeletable : IDeletable + public class TestInvalidDeletable : IDeletable { public Guid Id { get; set; } public string? DeletedBy { get; set; } diff --git a/test/E13.Common.Data.Db.Tests/Sample/TestModifiable.cs b/test/E13.Common.Data.Db.Tests/Sample/TestModifiable.cs index 58dd784..8cb39c2 100644 --- a/test/E13.Common.Data.Db.Tests/Sample/TestModifiable.cs +++ b/test/E13.Common.Data.Db.Tests/Sample/TestModifiable.cs @@ -7,7 +7,7 @@ namespace E13.Common.Data.Db.Tests.Sample { - public class TestModifiable : IModifiable + public class TestModifiable : IModifiable { public Guid Id { get; set; } public string Text { get; set; } = string.Empty; diff --git a/test/E13.Common.Data.Db.Tests/Sample/TestOwnable.cs b/test/E13.Common.Data.Db.Tests/Sample/TestOwnable.cs index d102748..bc7e3ae 100644 --- a/test/E13.Common.Data.Db.Tests/Sample/TestOwnable.cs +++ b/test/E13.Common.Data.Db.Tests/Sample/TestOwnable.cs @@ -10,8 +10,8 @@ namespace E13.Common.Data.Db.Tests.Sample public class TestOwnable : IOwnable { public Guid Id { get; set; } - public string OwnedBy { get; set; } = "OwnedBy"; - public string OwnedSource { get; set; } = "OwnedSource"; - public DateTime Owned { get; set; } + public string? OwnedBy { get; set; } = "OwnedBy"; + public string? OwnedSource { get; set; } = "OwnedSource"; + public DateTime? Owned { get; set; } } } diff --git a/test/E13.Common.Domain.Tests/IDeletableTests.cs b/test/E13.Common.Domain.Tests/IDeletableTests.cs index 8a5ae0a..71b57e5 100644 --- a/test/E13.Common.Domain.Tests/IDeletableTests.cs +++ b/test/E13.Common.Domain.Tests/IDeletableTests.cs @@ -10,13 +10,13 @@ public class IDeletableTests [Test] public void IsDeleted_DeletedNull_False() { - IDeletable arranged = new IDeletableSample { Deleted = null }; + IDeletable arranged = new IDeletableSample { Deleted = null }; arranged.IsDeleted(); } } - public class IDeletableSample : IDeletable + public class IDeletableSample : IDeletable { public Guid Id { get; set; } public string? DeletedBy { get; set; }