Sorry everyone, I had to publish my decision earlier, but for some reason I did not, so here it is.
BUT
Keep in mind that there may be something wrong with the solution, because it has not been verified by anyone or proven by the manufacturing process, maybe here I will get feedback.
In the project, I used ASP.NET Core 1
About my DB structure. I have 2 contexts. The first contains information about users (including the database schema that they should access), the second contains data related to a specific user.
In Startup.cs I add both contexts
public void ConfigureServices(IServiceCollection services.AddEntityFrameworkNpgsql() .AddDbContext<SharedDbContext>(options => options.UseNpgsql(Configuration["MasterConnection"])) .AddDbContext<DomainDbContext>((serviceProvider, options) => options.UseNpgsql(Configuration["MasterConnection"]) .UseInternalServiceProvider(serviceProvider)); ... services.Replace(ServiceDescriptor.Singleton<IModelCacheKeyFactory, MultiTenantModelCacheKeyFactory>()); services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
Note the use of UseInternalServiceProvider , it was suggested by Nero Sule with the following explanation
At the very end of the EFC 1 release cycle, the EF team decided to remove the EF services from the default service collection (AddEntityFramework (). AddDbContext ()), which means that services are resolved using the native EF service provider, not the application service. provider.
To force EF to use the application service provider instead, you must first add the EF services along with the data provider to the service collection, and then configure DBContext to use the internal service provider.
Now we need MultiTenantModelCacheKeyFactory
public class MultiTenantModelCacheKeyFactory : ModelCacheKeyFactory { private string _schemaName; public override object Create(DbContext context) { var dataContext = context as DomainDbContext; if(dataContext != null) { _schemaName = dataContext.SchemaName; } return new MultiTenantModelCacheKey(_schemaName, context); } }
where DomainDbContext is a context with user data
public class MultiTenantModelCacheKey : ModelCacheKey { private readonly string _schemaName; public MultiTenantModelCacheKey(string schemaName, DbContext context) : base(context) { _schemaName = schemaName; } public override int GetHashCode() { return _schemaName.GetHashCode(); } }
Also, we need to slightly change the context itself so that it takes into account the scheme:
public class DomainDbContext : IdentityDbContext<ApplicationUser> { public readonly string SchemaName; public DbSet<Foo> Foos{ get; set; } public DomainDbContext(ICompanyProvider companyProvider, DbContextOptions<DomainDbContext> options) : base(options) { SchemaName = companyProvider.GetSchemaName(); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasDefaultSchema(SchemaName); base.OnModelCreating(modelBuilder); } }
and the general context is strictly tied to a shared schema:
public class SharedDbContext : IdentityDbContext<ApplicationUser> { private const string SharedSchemaName = "shared"; public DbSet<Foo> Foos{ get; set; } public SharedDbContext(DbContextOptions<SharedDbContext> options) : base(options) {} protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasDefaultSchema(SharedSchemaName); base.OnModelCreating(modelBuilder); } }
ICompanyProvider is responsible for obtaining the user schema name. And yes, I know how far this is from perfect code.
public interface ICompanyProvider { string GetSchemaName(); } public class CompanyProvider : ICompanyProvider { private readonly SharedDbContext _context; private readonly IHttpContextAccessor _accesor; private readonly UserManager<ApplicationUser> _userManager; public CompanyProvider(SharedDbContext context, IHttpContextAccessor accesor, UserManager<ApplicationUser> userManager) { _context = context; _accesor = accesor; _userManager = userManager; } public string GetSchemaName() { Task<ApplicationUser> getUserTask = null; Task.Run(() => { getUserTask = _userManager.GetUserAsync(_accesor.HttpContext?.User); }).Wait(); var user = getUserTask.Result; if(user == null) { return "shared"; } return _context.Companies.Single(c => c.Id == user.CompanyId).SchemaName; } }
And if I did not miss anything, then this. Now, in each request of the authenticated user, the corresponding context will be used.
I hope this helps.