Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 3 additions & 35 deletions src/E13.Common.Data.Db/BaseDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

namespace E13.Common.Data.Db
{
public abstract class BaseDbContext : DbContext, IAuditContext
public abstract class BaseDbContext<T> : DbContext, IAuditContext<T>
{
/// <summary>
/// The user name used when the user name is null
Expand All @@ -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; }

Expand All @@ -42,41 +42,9 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasQueryFiltersFor<IDeletable>(e => e.Deleted != null);
modelBuilder.HasQueryFiltersFor<IDeletable<T>>(e => e.Deleted != null);

base.OnModelCreating(modelBuilder);
}

/// <summary>
/// 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.
/// </summary>
/// <returns></returns>
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();
}

}
}
4 changes: 2 additions & 2 deletions src/E13.Common.Data.Db/IAuditContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

namespace E13.Common.Data.Db
{
public interface IAuditContext
public interface IAuditContext<T>
{
string? AuditUser { get; }
T AuditUser { get; }
string? Source { get; }
}
}
10 changes: 5 additions & 5 deletions src/E13.Common.Data.Db/Interceptors/CreatableInterceptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@

namespace E13.Common.Data.Db.Interceptors
{
public sealed class CreatableInterceptor : SaveChangesInterceptor
public sealed class CreatableInterceptor<T> : SaveChangesInterceptor
{
public override InterceptionResult<int> SavingChanges(
DbContextEventData eventData,
InterceptionResult<int> result)
{
var auditContext = eventData.Context as IAuditContext ?? throw new Exception("Audit context is not set.");
var auditContext = eventData.Context as IAuditContext<T> ?? throw new Exception("Audit context is not set.");

HandleEventData(eventData, auditContext);

Expand All @@ -28,7 +28,7 @@ public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
var auditContext = eventData.Context as IAuditContext ?? throw new Exception("Audit context is not set.");
var auditContext = eventData.Context as IAuditContext<T> ?? throw new Exception("Audit context is not set.");

HandleEventData(eventData, auditContext);

Expand All @@ -40,9 +40,9 @@ public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
/// </summary>
/// <param name="eventData"></param>
/// <param name="auditContext"></param>
private static void HandleEventData(DbContextEventData eventData, IAuditContext auditContext)
private static void HandleEventData(DbContextEventData eventData, IAuditContext<T> auditContext)
{
foreach (var entry in eventData.Context!.ChangeTracker.Entries<ICreatable>()
foreach (var entry in eventData.Context!.ChangeTracker.Entries<ICreatable<T>>()
.Where(e => e.State == EntityState.Added))
{
entry.Entity.Created = DateTime.UtcNow;
Expand Down
10 changes: 5 additions & 5 deletions src/E13.Common.Data.Db/Interceptors/ModifiableInterceptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@

namespace E13.Common.Data.Db.Interceptors
{
public sealed class ModifiableInterceptor : SaveChangesInterceptor
public sealed class ModifiableInterceptor<T> : SaveChangesInterceptor
{
public override InterceptionResult<int> SavingChanges(
DbContextEventData eventData,
InterceptionResult<int> result)
{
var auditContext = eventData.Context as IAuditContext ?? throw new Exception("Audit context is not set.");
var auditContext = eventData.Context as IAuditContext<T> ?? throw new Exception("Audit context is not set.");

HandleEventData(eventData, auditContext);

Expand All @@ -29,7 +29,7 @@ public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
var auditContext = eventData.Context as IAuditContext ?? throw new Exception("Audit context is not set.");
var auditContext = eventData.Context as IAuditContext<T> ?? throw new Exception("Audit context is not set.");

HandleEventData(eventData, auditContext);

Expand All @@ -41,9 +41,9 @@ public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
/// </summary>
/// <param name="eventData"></param>
/// <param name="auditContext"></param>
private static void HandleEventData(DbContextEventData eventData, IAuditContext auditContext)
private static void HandleEventData(DbContextEventData eventData, IAuditContext<T> auditContext)
{
foreach (var entry in eventData.Context!.ChangeTracker.Entries<IModifiable>()
foreach (var entry in eventData.Context!.ChangeTracker.Entries<IModifiable<T>>()
.Where(e => e.State is EntityState.Added or EntityState.Modified))
{
entry.Entity.Modified = DateTime.UtcNow;
Expand Down
13 changes: 6 additions & 7 deletions src/E13.Common.Data.Db/Interceptors/SoftDeleteInterceptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> : SaveChangesInterceptor
{
public override InterceptionResult<int> SavingChanges(
DbContextEventData eventData,
InterceptionResult<int> result)
{
var auditContext = eventData.Context as IAuditContext ?? throw new Exception("Audit context is not set.");
var auditContext = eventData.Context as IAuditContext<T> ?? throw new Exception("Audit context is not set.");

HandleEventData(eventData, auditContext);

Expand All @@ -29,7 +28,7 @@
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
var auditContext = eventData.Context as IAuditContext ?? throw new Exception("Audit context is not set.");
var auditContext = eventData.Context as IAuditContext<T> ?? throw new Exception("Audit context is not set.");

HandleEventData(eventData, auditContext);

Expand All @@ -41,9 +40,9 @@
/// </summary>
/// <param name="eventData"></param>
/// <param name="auditContext"></param>
private static void HandleEventData(DbContextEventData eventData, IAuditContext auditContext)
private static void HandleEventData(DbContextEventData eventData, IAuditContext<T> auditContext)
{
foreach (var entry in eventData.Context!.ChangeTracker.Entries<IDeletable>())
foreach (var entry in eventData.Context!.ChangeTracker.Entries<IDeletable<T>>())
{
if (entry.State == EntityState.Deleted)
{
Expand All @@ -59,7 +58,7 @@
{
// “undelete” scenario
entry.Entity.Deleted = null;
entry.Entity.DeletedBy = null;
entry.Entity.DeletedBy = default(T);

Check warning on line 61 in src/E13.Common.Data.Db/Interceptors/SoftDeleteInterceptor.cs

View workflow job for this annotation

GitHub Actions / build (macOS-latest)

Possible null reference assignment.

Check warning on line 61 in src/E13.Common.Data.Db/Interceptors/SoftDeleteInterceptor.cs

View workflow job for this annotation

GitHub Actions / build (macOS-latest)

Possible null reference assignment.

Check warning on line 61 in src/E13.Common.Data.Db/Interceptors/SoftDeleteInterceptor.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Possible null reference assignment.

Check warning on line 61 in src/E13.Common.Data.Db/Interceptors/SoftDeleteInterceptor.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Possible null reference assignment.

Check warning on line 61 in src/E13.Common.Data.Db/Interceptors/SoftDeleteInterceptor.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Possible null reference assignment.

Check warning on line 61 in src/E13.Common.Data.Db/Interceptors/SoftDeleteInterceptor.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Possible null reference assignment.
entry.Entity.DeletedSource = null;
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/E13.Common.Domain/ICreatable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

namespace E13.Common.Domain
{
public interface ICreatable : IEntity
public interface ICreatable<T> : IEntity
{
string? CreatedBy { get; set; }
T? CreatedBy { get; set; }
string? CreatedSource { get; set; }
DateTime? Created { get; set; }
}
Expand Down
4 changes: 2 additions & 2 deletions src/E13.Common.Domain/IDeletable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

namespace E13.Common.Domain
{
public interface IDeletable : IEntity
public interface IDeletable<T> : IEntity
{
string? DeletedBy { get; set; }
T DeletedBy { get; set; }
string? DeletedSource { get; set; }
DateTime? Deleted { get; set; }

Expand Down
4 changes: 2 additions & 2 deletions src/E13.Common.Domain/IEffectable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

namespace E13.Common.Domain
{
public interface IEffectable : IEntity
public interface IEffectable<T> : IEntity
{
string? EffectiveBy { get; set; }
T EffectiveBy { get; set; }
string? EffectiveSource { get; set; }
DateTime? Effective { get; set; }
}
Expand Down
8 changes: 4 additions & 4 deletions src/E13.Common.Domain/IExpirable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

namespace E13.Common.Domain
{
public interface IExpirable : IEntity
public interface IExpirable<T> : 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; }
}
}
4 changes: 2 additions & 2 deletions src/E13.Common.Domain/IModifiable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

namespace E13.Common.Domain
{
public interface IModifiable : IEntity
public interface IModifiable<T> : IEntity
{
string? ModifiedBy { get; set; }
T ModifiedBy { get; set; }
string? ModifiedSource { get; set; }
DateTime? Modified { get; set; }
}
Expand Down
4 changes: 2 additions & 2 deletions src/E13.Common.Domain/IOwnable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace E13.Common.Domain
public interface IOwnable<T> : IEntity
{
T OwnedBy { get; set; }
string OwnedSource { get; set; }
DateTime Owned { get; set; }
string? OwnedSource { get; set; }
DateTime? Owned { get; set; }
}
}
18 changes: 9 additions & 9 deletions test/E13.Common.Data.Db.Tests/BaseDbContext_ICreatableTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ public class BaseDbContext_ICreatableTests
public void Setup()
{
var services = new ServiceCollection();
services.AddScoped<CreatableInterceptor>();
services.AddScoped<ModifiableInterceptor>();
services.AddScoped<SoftDeleteInterceptor>();
services.AddDbContext<IAuditContext, TestDbContext>((sp, o) =>
services.AddScoped<CreatableInterceptor<string>>();
services.AddScoped<ModifiableInterceptor<string>>();
services.AddScoped<SoftDeleteInterceptor<string>>();
services.AddDbContext<IAuditContext<string>, TestDbContext>((sp, o) =>
{
o.UseInMemoryDatabase($"{Guid.NewGuid()}");
o.EnableSensitiveDataLogging();
o.AddInterceptors(
sp.GetRequiredService<CreatableInterceptor>(),
sp.GetRequiredService<ModifiableInterceptor>(),
sp.GetRequiredService<SoftDeleteInterceptor>());
sp.GetRequiredService<CreatableInterceptor<string>>(),
sp.GetRequiredService<ModifiableInterceptor<string>>(),
sp.GetRequiredService<SoftDeleteInterceptor<string>>());
});

Context = services.BuildServiceProvider().GetService<TestDbContext>();
Expand Down Expand Up @@ -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<string>.UnknownUser);
}

[Test]
Expand All @@ -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<string>.UnknownUser);
}

[Test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ public void Setup()
var services = new ServiceCollection();
services.AddDbContext<TestDbContext>(o => {
o.UseInMemoryDatabase($"{Guid.NewGuid()}");
o.AddInterceptors(new CreatableInterceptor());
o.AddInterceptors(new ModifiableInterceptor());
o.AddInterceptors(new SoftDeleteInterceptor());
o.AddInterceptors(new CreatableInterceptor<string>());
o.AddInterceptors(new ModifiableInterceptor<string>());
o.AddInterceptors(new SoftDeleteInterceptor<string>());
});

Context = services.BuildServiceProvider().GetService<TestDbContext>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ public void Setup()
var services = new ServiceCollection();
services.AddDbContext<TestDbContext>(o => {
o.UseInMemoryDatabase($"{Guid.NewGuid()}");
o.AddInterceptors(new CreatableInterceptor());
o.AddInterceptors(new ModifiableInterceptor());
o.AddInterceptors(new SoftDeleteInterceptor());
o.AddInterceptors(new CreatableInterceptor<string>());
o.AddInterceptors(new ModifiableInterceptor<string>());
o.AddInterceptors(new SoftDeleteInterceptor<string>());
});

Context = services.BuildServiceProvider().GetService<TestDbContext>();
Expand Down Expand Up @@ -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<string>.UnknownUser);
}

[Test]
Expand Down
2 changes: 1 addition & 1 deletion test/E13.Common.Data.Db.Tests/Sample/TestCreatable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace E13.Common.Data.Db.Tests.Sample
{
public class TestCreatable : ICreatable
public class TestCreatable : ICreatable<string>
{
public Guid Id { get; set; }
public string Text { get; set; } = string.Empty;
Expand Down
7 changes: 5 additions & 2 deletions test/E13.Common.Data.Db.Tests/Sample/TestDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace E13.Common.Data.Db.Tests.Sample
{
public class TestDbContext : BaseDbContext
public class TestDbContext : BaseDbContext<string>
{
public DbSet<TestCreatable> Creatables { get; set; }
public DbSet<TestModifiable> Modifiables { get; set; }
Expand All @@ -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
Expand Down Expand Up @@ -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();
}
}
}
2 changes: 1 addition & 1 deletion test/E13.Common.Data.Db.Tests/Sample/TestDeletable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@

namespace E13.Common.Data.Db.Tests.Sample
{
public class TestDeletable : IDeletable
public class TestDeletable : IDeletable<string>
{
public Guid Id { get; set; }
public string? DeletedBy { get; set; }

Check warning on line 13 in test/E13.Common.Data.Db.Tests/Sample/TestDeletable.cs

View workflow job for this annotation

GitHub Actions / build (macOS-latest)

Nullability of reference types in return type of 'string? TestDeletable.DeletedBy.get' doesn't match implicitly implemented member 'string IDeletable<string>.DeletedBy.get' (possibly because of nullability attributes).

Check warning on line 13 in test/E13.Common.Data.Db.Tests/Sample/TestDeletable.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Nullability of reference types in return type of 'string? TestDeletable.DeletedBy.get' doesn't match implicitly implemented member 'string IDeletable<string>.DeletedBy.get' (possibly because of nullability attributes).

Check warning on line 13 in test/E13.Common.Data.Db.Tests/Sample/TestDeletable.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Nullability of reference types in return type of 'string? TestDeletable.DeletedBy.get' doesn't match implicitly implemented member 'string IDeletable<string>.DeletedBy.get' (possibly because of nullability attributes).
public string? DeletedSource { get; set; }
public DateTime? Deleted { get; set; }
}
Expand Down
Loading
Loading