Skip to content

Instantly share code, notes, and snippets.

@kirides
Last active January 23, 2025 13:30
Show Gist options
  • Save kirides/1300eff02afd3e450c3af3405dcff2e5 to your computer and use it in GitHub Desktop.
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
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