-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add simple EF entity stamping hook
- Loading branch information
Showing
9 changed files
with
338 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ | |
"vite", | ||
"unplugin", | ||
"IIFE", | ||
"Stampable", | ||
|
||
"POCOs", | ||
"dtos", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletions
1
src/IntelliTect.Coalesce.AuditLogging/Internal/AuditInterceptor.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
131 changes: 131 additions & 0 deletions
131
src/IntelliTect.Coalesce.Tests/Tests/EntityStampingTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
using IntelliTect.Coalesce.Tests.TargetClasses.TestDbContext; | ||
using Microsoft.EntityFrameworkCore; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using System; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using Xunit; | ||
|
||
#nullable enable | ||
|
||
namespace IntelliTect.Coalesce.AuditLogging.Tests; | ||
|
||
public class EntityStampingTests | ||
{ | ||
public AppDbContext Db { get; } | ||
|
||
class TestService | ||
{ | ||
public string TestValue => "42"; | ||
} | ||
|
||
public EntityStampingTests() | ||
{ | ||
var appSp = new ServiceCollection() | ||
.AddSingleton<TestService>() | ||
.BuildServiceProvider(); | ||
|
||
Db = new AppDbContext(new DbContextOptionsBuilder<AppDbContext>() | ||
.UseInMemoryDatabase(Guid.NewGuid().ToString()) | ||
.UseApplicationServiceProvider(appSp) | ||
.Options | ||
); | ||
} | ||
|
||
[Theory] | ||
[InlineData(false)] | ||
[InlineData(true)] | ||
public async Task WithCustomApplicationService_SetsStampValue(bool async) | ||
{ | ||
var appSp = new ServiceCollection() | ||
.AddSingleton<TestService>() | ||
.BuildServiceProvider(); | ||
|
||
// Arrange | ||
using var db = BuildDbContext(b => b | ||
.UseStamping<Case, TestService>((obj, service) => obj.Title = service?.TestValue) | ||
.UseApplicationServiceProvider(appSp) | ||
); | ||
|
||
// Act | ||
db.Cases.Add(new Case()); | ||
if (async) await db.SaveChangesAsync(); | ||
else db.SaveChanges(); | ||
|
||
// Assert | ||
var entity = db.Cases.Single(); | ||
Assert.Equal("42", entity.Title); | ||
} | ||
|
||
[Theory] | ||
[InlineData(false)] | ||
[InlineData(true)] | ||
public async Task WhenUserUnavailable_InjectsNull(bool async) | ||
{ | ||
// Arrange | ||
using var db = BuildDbContext(b => b | ||
.UseStamping<Case>((obj, user) => obj.Title = user?.ToString() ?? "null") | ||
); | ||
|
||
// Act | ||
db.Cases.Add(new Case()); | ||
if (async) await db.SaveChangesAsync(); | ||
else db.SaveChanges(); | ||
|
||
// Assert | ||
var entity = db.Cases.Single(); | ||
Assert.Equal("null", entity.Title); | ||
} | ||
|
||
[Theory] | ||
[InlineData(false)] | ||
[InlineData(true)] | ||
public async Task WhenServiceUnavailable_InjectsNull(bool async) | ||
{ | ||
// Arrange | ||
using var db = BuildDbContext(b => b | ||
.UseStamping<Case, TestService>((obj, service) => obj.Title = service?.TestValue ?? "null") | ||
); | ||
|
||
// Act | ||
db.Cases.Add(new Case()); | ||
if (async) await db.SaveChangesAsync(); | ||
else db.SaveChanges(); | ||
|
||
// Assert | ||
var entity = db.Cases.Single(); | ||
Assert.Equal("null", entity.Title); | ||
} | ||
|
||
[Theory] | ||
[InlineData(false)] | ||
[InlineData(true)] | ||
public async Task CanInjectContext(bool async) | ||
{ | ||
// Arrange | ||
using var db = BuildDbContext(b => b | ||
.UseStamping<Case, AppDbContext>((obj, db) => obj.Title = db!.Database.ProviderName) | ||
); | ||
|
||
// Act | ||
db.Cases.Add(new Case()); | ||
if (async) await db.SaveChangesAsync(); | ||
else db.SaveChanges(); | ||
|
||
// Assert | ||
var entity = db.Cases.Single(); | ||
Assert.Equal("Microsoft.EntityFrameworkCore.InMemory", entity.Title); | ||
} | ||
|
||
private AppDbContext BuildDbContext(Func<DbContextOptionsBuilder<AppDbContext>, DbContextOptionsBuilder> setup) | ||
{ | ||
var builder = new DbContextOptionsBuilder<AppDbContext>() | ||
.UseInMemoryDatabase(Guid.NewGuid().ToString()); | ||
|
||
var db = new AppDbContext(setup(builder).Options); | ||
|
||
db.Database.EnsureCreated(); | ||
|
||
return db; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
using IntelliTect.Coalesce.Utilities; | ||
using Microsoft.EntityFrameworkCore; | ||
using Microsoft.EntityFrameworkCore.Diagnostics; | ||
using System; | ||
using System.Data; | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace IntelliTect.Coalesce.Stamping; | ||
|
||
internal class StampInterceptor<TStampable> : SaveChangesInterceptor | ||
where TStampable : class | ||
{ | ||
public Delegate Action { get; } | ||
public Type[] Services { get; } | ||
|
||
public StampInterceptor(Delegate action, Type[] services) | ||
{ | ||
Action = action; | ||
Services = services; | ||
} | ||
|
||
public override ValueTask<InterceptionResult<int>> SavingChangesAsync( | ||
DbContextEventData eventData, | ||
InterceptionResult<int> result, | ||
CancellationToken cancellationToken = default) | ||
{ | ||
return ValueTask.FromResult(SavingChanges(eventData, result)); | ||
} | ||
|
||
public override InterceptionResult<int> SavingChanges( | ||
DbContextEventData eventData, | ||
InterceptionResult<int> result) | ||
{ | ||
var db = eventData.Context!; | ||
var provider = new EntityFrameworkServiceProvider(db); | ||
|
||
var services = Services | ||
.Select(t => t.IsAssignableFrom(db.GetType()) | ||
? db | ||
: provider.GetService(t)) | ||
.ToArray(); | ||
|
||
var entities = db.ChangeTracker | ||
.Entries<TStampable>() | ||
.Where(x => x.State is EntityState.Added or EntityState.Modified) | ||
.ToList(); | ||
|
||
foreach (var entry in entities) | ||
{ | ||
Action.DynamicInvoke([entry.Entity, .. services]); | ||
} | ||
|
||
return result; | ||
} | ||
} | ||
|
||
internal sealed class StampInterceptor<TStampable, TService> : StampInterceptor<TStampable> | ||
where TStampable : class | ||
{ | ||
public StampInterceptor(Action<TStampable, TService?> action) | ||
: base(action, [typeof(TService)]) { } | ||
} | ||
|
||
internal sealed class StampInterceptor<TStampable, TService1, TService2> : StampInterceptor<TStampable> | ||
where TStampable : class | ||
{ | ||
public StampInterceptor(Action<TStampable, TService1?, TService2?> action) | ||
: base(action, [typeof(TService1), typeof(TService2)]) { } | ||
} |
74 changes: 74 additions & 0 deletions
74
src/IntelliTect.Coalesce/Stamping/StampingDbContextOptionsBuilderExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
using IntelliTect.Coalesce.Stamping; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.EntityFrameworkCore; | ||
using System; | ||
using System.Security.Claims; | ||
|
||
namespace IntelliTect.Coalesce; | ||
|
||
public static class StampingDbContextOptionsBuilderExtensions | ||
{ | ||
/// <summary> | ||
/// Adds a save interceptor that will perform a simple action during <see cref="DbContext.SaveChanges()"/> | ||
/// on all <see cref="EntityState.Added"/> or <see cref="EntityState.Modified"/> entities. | ||
/// The action will receive the <see cref="ClaimsPrincipal"/> from the current <see cref="HttpContext"/>. | ||
/// </summary> | ||
/// <typeparam name="TStampable">The type of entity to be stamped. Can be a base class or interface.</typeparam> | ||
/// <param name="builder">The db context options to configure.</param> | ||
/// <param name="stampAction">The action to take against the entity.</param> | ||
public static DbContextOptionsBuilder UseStamping<TStampable>( | ||
this DbContextOptionsBuilder builder, | ||
Action<TStampable, ClaimsPrincipal?> stampAction | ||
) | ||
where TStampable : class | ||
{ | ||
builder.AddInterceptors(new StampInterceptor<TStampable, IHttpContextAccessor>((target, accessor) => | ||
{ | ||
var user = accessor?.HttpContext?.User; | ||
stampAction(target, user); | ||
})); | ||
|
||
return builder; | ||
} | ||
|
||
/// <summary> | ||
/// Adds a save interceptor that will perform a simple action during <see cref="DbContext.SaveChanges()"/> | ||
/// on all <see cref="EntityState.Added"/> or <see cref="EntityState.Modified"/> entities. | ||
/// The action will receive the service specified by <typeparamref name="TService"/>. | ||
/// </summary> | ||
/// <typeparam name="TStampable">The type of entity to be stamped. Can be a base class or interface.</typeparam> | ||
/// <typeparam name="TService">The service to dependency inject into the action.</typeparam> | ||
/// <param name="builder">The db context options to configure.</param> | ||
/// <param name="stampAction">The action to take against the entity.</param> | ||
public static DbContextOptionsBuilder UseStamping<TStampable, TService>( | ||
this DbContextOptionsBuilder builder, | ||
Action<TStampable, TService?> stampAction | ||
) | ||
where TStampable : class | ||
{ | ||
builder.AddInterceptors(new StampInterceptor<TStampable, TService>(stampAction)); | ||
|
||
return builder; | ||
} | ||
|
||
/// <summary> | ||
/// Adds a save interceptor that will perform a simple action during <see cref="DbContext.SaveChanges()"/> | ||
/// on all <see cref="EntityState.Added"/> or <see cref="EntityState.Modified"/> entities. | ||
/// The action will receive the services specified by <typeparamref name="TService1"/> and <typeparamref name="TService2"/>. | ||
/// </summary> | ||
/// <typeparam name="TStampable">The type of entity to be stamped. Can be a base class or interface.</typeparam> | ||
/// <typeparam name="TService1">A service to dependency inject into the action.</typeparam> | ||
/// <typeparam name="TService2">A second service to dependency inject into the action.</typeparam> | ||
/// <param name="builder">The db context options to configure.</param> | ||
/// <param name="stampAction">The action to take against the entity.</param> | ||
public static DbContextOptionsBuilder UseStamping<TStampable, TService1, TService2>( | ||
this DbContextOptionsBuilder builder, | ||
Action<TStampable, TService1?, TService2?> stampAction | ||
) | ||
where TStampable : class | ||
{ | ||
builder.AddInterceptors(new StampInterceptor<TStampable, TService1, TService2>(stampAction)); | ||
|
||
return builder; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters