运行时使用带有不同IEntityTypeConfiguration实现的不同DbContextOptions创建同一DbContext类的多个实例
嘿,我正好折腾过类似的场景,给你好好捋捋怎么实现!你的需求核心就是:同一个DbContext类,针对不同的运行时添加的数据库(每个都有专属ID和连接串,还带用户自定义字段),根据用户Claim里的数据库ID,用对应的DbContextOptions(里面绑定了专属的IEntityTypeConfiguration配置)来创建实例对吧?下面是具体的实现思路和代码示例:
第一步:搭好可扩展的实体配置框架
因为每个数据库有自己的自定义字段,不能把所有配置硬写死在DbContext里。你可以给每个数据库ID关联一套IEntityTypeConfiguration实现,先搞个基础配置覆盖所有数据库共有的字段,再给每个数据库写补充的自定义配置。
举个例子,假设你的核心实体是User,基础配置可以这么写:public class BaseUserConfiguration : IEntityTypeConfiguration<User> { public void Configure(EntityTypeBuilder<User> builder) { // 所有数据库都有的核心字段配置 builder.HasKey(u => u.Id); builder.Property(u => u.UserName).IsRequired().HasMaxLength(50); } }那某一个数据库的自定义配置就可以继承基础配置的逻辑,再加上专属字段:
public class CustomUserConfigForDb001 : IEntityTypeConfiguration<User> { public void Configure(EntityTypeBuilder<User> builder) { // 先应用基础配置 new BaseUserConfiguration().Configure(builder); // 添加这个数据库独有的自定义字段 builder.Property<string>("UserCustomTag").HasMaxLength(100); } }第二步:缓存每个数据库对应的DbContextOptions
你需要一个缓存容器,比如用ASP.NET Core自带的IMemoryCache,或者自己搞个字典来存,key就是数据库ID,value是对应的DbContextOptions<YourDbContext>。当用户添加新数据库时,就生成对应的配置,构建好DbContextOptions丢进缓存里。
构建的时候要把连接串和该数据库的所有配置都绑定进去,这里要注意处理非泛型的IEntityTypeConfiguration,得整个扩展方法:public async Task RegisterDatabaseAsync(string dbId, string connectionString, IEnumerable<IEntityTypeConfiguration> customConfigs) { var optionsBuilder = new DbContextOptionsBuilder<YourDbContext>(); optionsBuilder.UseSqlServer(connectionString) .ConfigureModel((modelBuilder, _) => { // 先加基础配置 modelBuilder.ApplyConfiguration(new BaseUserConfiguration()); // 再逐个应用当前数据库的自定义配置 foreach (var config in customConfigs) { modelBuilder.ApplyNonGenericConfig(config); } }); // 把配置缓存起来 await _memoryCache.SetAsync(dbId, optionsBuilder.Options); } // 自定义扩展方法,处理非泛型的IEntityTypeConfiguration public static class ModelBuilderExtensions { public static void ApplyNonGenericConfig(this ModelBuilder modelBuilder, IEntityTypeConfiguration config) { // 通过反射拿到泛型参数 var entityType = config.GetType().GetInterfaces() .First(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEntityTypeConfiguration<>)) .GetGenericArguments()[0]; // 调用泛型版的ApplyConfiguration var applyMethod = typeof(ModelBuilder).GetMethod(nameof(ModelBuilder.ApplyConfiguration))! .MakeGenericMethod(entityType); applyMethod.Invoke(modelBuilder, new[] { config }); } }第三步:根据用户Claim获取对应的DbContext
当需要创建DbContext实例时,从用户的Claim里抠出数据库ID,去缓存里找对应的DbContextOptions,直接new实例就行:public YourDbContext GetCurrentDbContext(ClaimsPrincipal user) { var dbId = user.FindFirst("DatabaseId")?.Value; if (string.IsNullOrWhiteSpace(dbId) || !_memoryCache.TryGetValue(dbId, out DbContextOptions<YourDbContext> dbOptions)) { throw new InvalidOperationException("找不到对应数据库的配置信息,请检查Claim或数据库注册状态"); } return new YourDbContext(dbOptions); }这里要提一嘴:DbContext本身是轻量级的,每次请求new一个完全没问题,缓存的
DbContextOptions是线程安全的,可以放心复用。第四步:处理动态自定义字段的场景
如果用户是在运行时直接添加自定义字段,而不是提前写好IEntityTypeConfiguration类,那你也可以不用实现接口,直接用ModelBuilder的动态API来配置:optionsBuilder.UseSqlServer(connectionString) .ConfigureModel((modelBuilder, _) => { modelBuilder.Entity<User>() .Property<string>("DynamicCustomField") .HasMaxLength(200) .IsRequired(false); });
最后再给你几个踩过坑的注意点:
DbContextOptions是不可变的,要是数据库的自定义配置有更新,得重新生成新的DbContextOptions替换缓存里的旧值- 多实例部署的话,别用
IMemoryCache,换成IDistributedCache,不然每个实例的缓存不一样会出问题 - 每个数据库对应的Model是独立的,EF Core会自动缓存Model,不用担心重复构建的性能问题
备注:内容来源于stack exchange,提问作者avenmore

