Last active
January 23, 2025 13:30
-
-
Save kirides/1300eff02afd3e450c3af3405dcff2e5 to your computer and use it in GitHub Desktop.
A Example on how Pooled DbContext instances may share their data with Interceptors
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
Microsoft.Data.Sqlite | |
Microsoft.EntityFrameworkCore | |
Microsoft.EntityFrameworkCore.ChangeTracking | |
Microsoft.EntityFrameworkCore.Diagnostics | |
Microsoft.EntityFrameworkCore.Infrastructure | |
Microsoft.Extensions.DependencyInjection | |
System.Threading.Tasks | |
void Main() | |
{ | |
try | |
{ | |
RunApp(); | |
} | |
finally | |
{ | |
SqliteConnection.ClearAllPools(); | |
} | |
} | |
// Example on HOW a singleton "getter"-registry makes it possible to grab dependencies if they are known | |
public class InterceptorDependenciesRegistry | |
{ | |
public static Dictionary<(Type, Type), Func<DbContextEventData, object?>> Providers { get; } = new() | |
{ | |
{(typeof(AppDbContext), typeof(UserContext)), evt => (evt.Context as AppDbContext)!.User }, | |
}; | |
} | |
public class SomeSaveChangesInterceptor: SaveChangesInterceptor | |
{ | |
public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result) | |
{ | |
var ctx = eventData.Context as AppDbContext; | |
// access ctx.User or some other "Scoped" thing | |
// if you REALLY do need to access arbitrary data from any dbcontext, a registry may make sense | |
if (InterceptorDependenciesRegistry.Providers.TryGetValue((eventData.Context!.GetType(), typeof(UserContext)), out var userContextProvider) | |
&& userContextProvider(eventData) is UserContext user) | |
{ | |
user.Dump(); | |
} | |
return base.SavingChanges(eventData, result); | |
} | |
} | |
void RunApp() | |
{ | |
using var services = new ServiceCollection() | |
.AddScoped<UserContextProvider>() | |
.AddTransient<SomeSaveChangesInterceptor>() | |
.AddPooledDbContextFactory<AppDbContext>( | |
(sp, o) => o.UseSqlite() | |
.AddInterceptors(sp.GetRequiredService<SomeSaveChangesInterceptor>()) | |
//.EnableSensitiveDataLogging() | |
//.EnableDetailedErrors() | |
//.LogTo(x => Console.WriteLine(x), Microsoft.Extensions.Logging.LogLevel.Error) | |
) | |
//.AddDbContextFactory<AppDbContext>( | |
// (sp, o) => o.UseSqlite() | |
// //.EnableSensitiveDataLogging() | |
// //.EnableDetailedErrors() | |
// //.LogTo(x => Console.WriteLine(x), Microsoft.Extensions.Logging.LogLevel.Error) | |
//) | |
.AddScoped<AppDbContextFactory>() | |
.AddScoped<AppDbContext>(x => x.GetRequiredService<AppDbContextFactory>().CreateDbContext()) | |
.BuildServiceProvider(true); | |
using (var scope = services.CreateScope()) | |
{ | |
scope.ServiceProvider.GetRequiredService<UserContextProvider>().User = new UserContext(); | |
using var db = scope.ServiceProvider.GetRequiredService<AppDbContext>(); | |
db.Database.EnsureDeleted(); | |
db.Database.EnsureCreated(); | |
db.SomeEntities.Add(new()); | |
db.SaveChanges(); | |
db.Set<SomeEntity>().Select(x => SqliteEF.UuidV7()).ToQueryString().Dump(); | |
} | |
} | |
void Benchmark(IServiceProvider services) | |
{ | |
using (var scope = services.CreateScope()) | |
{ | |
scope.ServiceProvider.GetRequiredService<UserContextProvider>().User = new UserContext(); | |
using var db = scope.ServiceProvider.GetRequiredService<AppDbContext>(); | |
db.SomeEntities.AsNoTracking().Any(); | |
} | |
} | |
public interface ITenantOwned | |
{ | |
Guid TenantId { get; } | |
} | |
public class SomeEntity : ITenantOwned | |
{ | |
public Guid Id { get; set; } | |
public Guid TenantId { get; set; } = Guid.NewGuid(); | |
} | |
public class UserContext | |
{ | |
public string Database { get; set; } = "app"; | |
public bool HasPermission(string permission) => true; | |
public Guid[] TenantsWithPermission(string permission) => Tenants; | |
public Guid[] Tenants { get; set; } = [Guid.NewGuid(), Guid.NewGuid()]; | |
public Dictionary<Guid, string[]> Permissions => Tenants.ToDictionary(x => x, x => new string[] { }); | |
} | |
public class UserContextProvider | |
{ | |
public UserContext? User { get; set; } | |
} | |
public class AppDbContextFactory( | |
IDbContextFactory<AppDbContext> factory, | |
UserContextProvider userContextProvider) | |
: IDbContextFactory<AppDbContext> | |
{ | |
public AppDbContext CreateDbContext() | |
{ | |
var db = factory.CreateDbContext(); | |
db.User = userContextProvider.User; | |
db.Database.SetConnectionString($"Data Source={db.User.Database}.db"); | |
if (db.Database.IsSqlite()) | |
{ | |
db.Database.GetDbConnection().StateChange += static (sender, state) => | |
{ | |
if (state.CurrentState == ConnectionState.Open) | |
{ | |
(sender as SqliteConnection).CreateFunction<Guid>("cf_uuid", static () => Guid.NewGuid()); | |
// (sender as SqliteConnection).CreateFunction<Guid>("cf_uuid", static () => UUIDNext.Uuid.NewDatabaseFriendly(Database.SQLite)); | |
} | |
}; | |
} | |
return db; | |
} | |
} | |
public class AppDbContext(DbContextOptions<AppDbContext> options) | |
: DbContext(options) | |
{ | |
public DbSet<SomeEntity> SomeEntities => Set<SomeEntity>(); | |
public UserContext? User { get; set; } | |
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | |
{ | |
base.OnConfiguring(optionsBuilder); | |
} | |
protected override void OnModelCreating(ModelBuilder modelBuilder) | |
{ | |
base.OnModelCreating(modelBuilder); | |
modelBuilder | |
.HasDbFunction(SqliteEF.UuidV7MethodInfo) | |
.HasName("cf_UUID"); | |
modelBuilder.Entity<SomeEntity>() | |
.Property(x => x.TenantId) | |
.HasSentinel(Guid.Empty) | |
.IsRequired(); | |
modelBuilder.Entity<SomeEntity>() | |
.ToTable(static o => | |
{ | |
var colTenantId = o.Metadata.FindProperty("TenantId").GetColumnName(); | |
o.HasCheckConstraint($"{o.Metadata.GetTableName()}.{colTenantId}.Not_Empty", $"replace(replace({colTenantId},'-',''),'0','')!=''"); | |
}); | |
modelBuilder.Entity<SomeEntity>() | |
.Property(x => x.Id) | |
.IsRequired() | |
.ValueGeneratedOnAdd() | |
.HasDefaultValueSql("cf_UUID()") // uses Sqlite.CreateFunction<Guid>(..., () => Guid.NewGuid()); | |
; | |
} | |
} | |
public static class SqliteEF | |
{ | |
internal static readonly MethodInfo UuidV7MethodInfo = typeof(SqliteEF).GetMethod(nameof(UuidV7))!; | |
public static Guid UuidV7() | |
{ | |
throw new InvalidOperationException("Only for use with Entity Framework"); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment