Dinamically changing schema in Entity Framework Core Npgsql - c#

I'm trying to change schema dynamically. Everytime a login happens I need to check, set, and if necessary change the schema to make my queries with Entity.
I found a question with the perfect solution but I'm trying to use this with Npgsql and I didn't find a way.
See the link below:
Dynamically changing schema in Entity Framework Core
My code till now:
public WebAppSchemaContext(DbContextOptions<WebAppSchemaContext> options, IConfiguration configuration) : base(options)
{
_configuration = configuration;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql(_configuration.GetConnectionString("DefaultConnection"));
// here I don't know what to do with Npgsql
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Pais>().ToTable("pais", Util.Schema);
...
Everytime a login happens:
public Login? ObterUsuarioPorCpf(string cpf, string empresa)
{
var empresaUser = webAppContext
.Set<Company>()
.FirstOrDefault(x => x.Empresa == empresa);
if (empresaUser == null)
return null;
Util.Schema = "emp" + empresaUser.Empresa;
And to do the queries I'm doing like this:
return webAppSchemaContext
.Set<Pais>()
.Select(p => new PaisDto
{
Codigo = p.NumCode,
Nome = p.NamePt
})
.ToList();
Does anyone knows how I can configure this on Npgsql?
Thanks.
Best regards.

Related

Applying EF Core migrations per tenant schema

I'm working with .NET Core 3.1 and EntityFramework Core 3.1.3.
I'm trying to implement tenant data separation using DB schemas. I've read this. I'm aware it's a little bit outdated, so I've adjusted.
I've created an implementation of DbContext:
public class AppDataContext : DbContext
{
private readonly ITenantProvider _tenantProvider;
public AppDataContext(DbContextOptions<AppDataContext> options, ITenantProvider tenantProvider) : base(options)
{
_tenantProvider = tenantProvider;
}
public DbSet<Book> Books { get; set; }
public DbSet<Comics> Comics { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Tenant schema mapping
var tenant = _tenantProvider.GetTenantString();
modelBuilder.HasDefaultSchema(tenant);
}
public static void ApplyMigrations(string connectionString, string tenant)
{
var optionsBuilder = new DbContextOptionsBuilder<AppDataContext>();
optionsBuilder.UseSqlServer(connectionString, x => x.MigrationsHistoryTable("__EFMigrationsHistory", tenant));
var ctx = new AppDataContext(optionsBuilder.Options, StaticTenantProvider.WithTenant(tenant));
ctx.Database.Migrate();
}
}
ITenantProvider is a registered scoped service in .NET Core DI. This AppDataContext is registered like so:
services.AddDbContext<AppDataContext>((ctx, opt) =>
{
opt.UseSqlServer(Configuration["SqlConnectionString"]);
});
Now, my idea would be, that anytime I want to provision another tenant, I would make call like this (database instance already exists and may have some tables/schemas already):
var connString = GetConnString(); // Where does connection string and tenant name come from is not important
var tenantName = GetTenantName();
AppDataContext.ApplyMigrations(connString, tenantName);
And after this I would have a new unique DB schema with all tables all set up for the Tenant.
Unfortunately, it's not working. Tables are still being created for default "dbo" schema (I'm using SqlServer).
I've started looking around the internet, first I've found this. It looked a bit odd for me, but tried anyway:
public static void ApplyMigrations(string connectionString, string tenant)
{
var optionsBuilder = new DbContextOptionsBuilder<AppDataContext>();
optionsBuilder.UseSqlServer(connectionString);
var ctx = new AppDataContext(optionsBuilder.Options, StaticTenantProvider.WithTenant(tenant));
var command = $"IF (NOT EXISTS (SELECT * FROM sys.schemas WHERE name = N'{tenant}')) " +
"BEGIN" +
$" EXEC ('CREATE SCHEMA {tenant}');" +
"END";
ctx.Database.ExecuteSqlRaw(command);
ctx.Database.Migrate();
}
Didn't work. I could see that the schema got created (using SSMS), but the tables still landed in "dbo".
Then I thought, that maybe it has problems with locating the Migration History table, so I tried MigrationsHistoryTable like this:
optionsBuilder.UseSqlServer(connectionString, x => x.MigrationsHistoryTable("__EFMigrationsHistory", tenant));
But no, Migration History table did get created in new schema, but all other tables still were in "dbo".
What am I missing? Or maybe it's not possible to use EF Migrations and Schema separation at the same time?
Thanks in advance.
EDIT: To make things clear: I'm using Code First approach. There are already generated migrations for AppDataContext.
You need to tell EF where to put each table by providing the schema in the configuration's IEntityTypeConfiguration.ToTable(...) overload.
First define a configuration for the entity:
public class SpecialSchemaEntityConfiguration : IEntityTypeConfiguration<SpecialSchemaEntity>
{
private readonly string _schema;
public SpecialSchemaEntityConfiguration(string schema) => this._schema = schema;
public void Configure(EntityTypeBuilder<SpecialSchemaEntity> builder)
{
builder.ToTable("SpecialSchemaEntity", this._schema);
}
}
Then, tell EF to use that configuration when building out the table:
public class AppDataContext : DbContext
{
private readonly string _schema = "special_schema";
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new SpecialSchemaEntityConfiguration(this._schema));
}
}

EF Core InMemoryDatabase - How to test with Keyless Entity (mapped to Sql View) - XUnit

Environment: .Net Core 3.1 REST API / EntityFrameworkCore.InMemory 3.1.6 / XUnit 2.4.1
In a Database First Setup I have a model mapped to a Sql View.
During Code Generation (with EF Core PowerTools 2.4.51) this entity is marked in DbContext with .HasNoKey()
When I try to test the endpoint accessing the DbSet mapped to the Sql View it throws exception:
Unable to track an instance of type '*' because it does not have a primary key. Only entity types with primary keys may be tracked.
Follows some code snippets with highlights of what I have tries so far.
Auto generated DbContext: ViewDemoAccountInfo is the entity mapped to a Sql View. Other entities are mapped to regular Sql Tables
// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
namespace Demo.Data.Entities
{
public partial class DemoDbContext : DbContext
{
public DemoDbContext(){}
public DemoDbContext(DbContextOptions<DemoDbContext> options): base(options){}
public virtual DbSet<ViewDemoAccountInfo> ViewDemoAccountInfo { get; set; }
// multiple other entities
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ViewDemoAccountInfo>(entity =>
{
entity.HasNoKey();
entity.ToView("ViewDemoAccountInfo");
entity.Property(e => e.AccountType).IsUnicode(false);
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
}
Attempt #1:
The test
public class MyIntegrationTests : BaseIntegrationTest {
// throws "Unable to track an instance of type 'ViewDemoAccountInfo'
// because it does not have a primary key. Only entity types with primary keys may be tracked."
[Fact]
public async Task SetupData_WhenKeylessEntity_ThenShouldNotThrow() {
using (var scope = _testServer.Host.Services.CreateScope()) {
var dbContext = scope.ServiceProvider.GetService<DemoDbContext>();
await dbContext.ViewDemoAccountInfo.AddAsync(MockedAccountInfo); // setup some data
await dbContext.SaveChangesAsync();
}
var endpointUrl = $"{ControllerRootUrl}/account-info";
var response = await _testClient.GetAsync(endpointUrl);
// Assertions
}
}
Helpers
public class BaseIntegrationTest {
protected readonly HttpClient _testClient;
protected readonly TestServer _testServer;
public BaseIntegrationTest() {
var builder = new WebHostBuilder()
.UseEnvironment("Test")
.ConfigureAppConfiguration((builderContext, config) => {
config.ConfigureSettings(builderContext.HostingEnvironment);
});
builder.ConfigureServices(services => {
services.ConfigureInMemoryDatabases(new InMemoryDatabaseRoot());
});
builder.UseStartup<Startup>();
_testServer = new TestServer(builder);
_testClient = _testServer.CreateClient();
}
}
// Regular DbContext
internal static class IntegrationExtensions {
public static void ConfigureInMemoryDatabases(this IServiceCollection services, InMemoryDatabaseRoot memoryDatabaseRoot) {
services.AddDbContext<DemoDbContext>(options =>
options.UseInMemoryDatabase("DemoApp", memoryDatabaseRoot)
.EnableServiceProviderCaching(false));
}
}
The simplest solution is to change the Auto generated DbContext and remove the .HasNoKey() config, but it would be removed each time the schema structure will be generated with EF Core PowerTools.
Search for other solutions which would not require changes in Auto generated files
Found: how to test keyless entity - github discussion planned for EF Core 5 and stackoverflow source
Attemp #2 - Try to create another DbContext and override the entity setup by adding explicitly a key when Database.IsInMemory
public class TestingDemoDbContext : DemoDbContext {
public TestingDemoDbContext(){}
public TestingDemoDbContext(DbContextOptions<DemoDbContext> options): base(options){}
protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ViewDemoAccountInfo>(entity => {
if (Database.IsInMemory()) {
entity.HasKey(e => new { e.AccountType, e.AccountStartDate });
}
});
}
}
In BaseIntegrationTest use the "extended" TestingDemoDbContext in ConfigureInMemoryDatabases method.
internal static class IntegrationExtensions {
public static void ConfigureInMemoryDatabases(this IServiceCollection services, InMemoryDatabaseRoot memoryDatabaseRoot) {
services.AddDbContext<TestingDemoDbContext>(options =>
options.UseInMemoryDatabase("DemoApp", memoryDatabaseRoot)
.EnableServiceProviderCaching(false));
}
}
The test is similar, with a small difference: var dbContext = scope.ServiceProvider.GetService<TestingDemoDbContext>();
Result - strange, but it throws The string argument 'connectionString' cannot be empty. - even I do use the InMemoryDatabase
Attemp #3 - Try to use the OnModelCreatingPartial method to add a Key for that keyless entity.
So, in the same namespace with the Regular DbContext, create the partial DbContext meant to enrich the existing config
namespace Demo.Data.Entities {
public partial class DemoDbContext : DbContext {
partial void OnModelCreatingPartial(ModelBuilder builder) {
builder.Entity<ViewDemoAccountInfo>(entity => {
// try to set a key when Database.IsInMemory()
entity.HasKey(e => new { e.AccountType, e.AccountStartDate }));
});
}
}
}
Result - The key { e.AccountType, e.AccountStartDate } cannot be added to keyless type 'ViewDemoAccountInfo'.
Any hints on how to add some mock data for Keyless entities (mapped to Sql View), with InMemoryDatabase, for testing purpose (with XUnit) would be grateful appreciated.
As well, if something is wrong or is considered bad practice in the setup I have listed here - would appreciate to receive improvement suggestions.
I know this is an old post, but I wanted to share the solution I ended up using for this in case anyone else comes across here.
In my model class, I added a [NotMapped] field named 'UnitTestKey'.
KeylessTable.cs
[Table("KeylessTable", Schema = "dbo")]
public class KeylessTable
{
[NotMapped]
public int UnitTestKey { get; set; }
[Column("FakeColName")]
public string FakeColumn { get; set; }
}
In my DbContext class, I use IHostEnvironment and used that to set HasKey() or HasNoKey() depending on if we are in the "Unit Testing" environment.
This example is using .NET 5. If using .NET Core 3.1 like in the original question, you would want to use IWebHostEnvironment.
ContextClass.cs
public class ContextClass : DbContext
{
private readonly IHostEnvironment _environment;
public ContextClass(DbContextOptions<ContextClass> options, IHostEnvironment environment) : base(options)
{
_environment = environment;
}
public DbSet<KeylessTable> KeylessTable => Set<KeylessTable>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<KeylessTable>(entity => {
if (_environment.EnvironmentName == "UnitTesting")
entity.HasKey(x => x.UnitTestKey);
else
entity.HasNoKey();
});
}
}
Then in my unit test, I mock the environment and set the name to be "UnitTesting".
UnitTest.cs
[Fact]
public async void GetKeylessTable_KeylessTableList()
{
// Arrange
var environment = new Mock<IHostEnvironment>();
environment.Setup(e => e.EnvironmentName).Returns("UnitTesting");
var options = new DbContextOptionsBuilder<ContextClass>().UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()).Options;
var contextStub = new ContextClass(options, environment.Object);
contextStub.Database.EnsureDeleted();
contextStub.Database.EnsureCreated();
contextStub.Set<KeylessTable>().AddRange(_keylessTablelMockData);
contextStub.SaveChanges();
var repository = new Repo(contextStub);
// Act
var response = await repository.GetKeylessData();
// Assert
response.Should().BeEquivalentTo(_keylessTablelMockData);
}

Adding new DbSet to DbContext when application has started and DbContext created

I have a project, in business it will creates table dynamicaly, its working with netcore3.0 and EF.
When an instance of dbcontext is created after dynamic table is created, I will use Assembly Emit to create a new type of the table, and use OnModelCreating method to add dbsets corresponding to tables.
public class ApplicationDbContext : IdentityDbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public virtual DbSet<Book> Books { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
//Use assmbly emit to create dynamic types
var types = CreateDynamicTypes();
foreach (var type in types)
{
builder.Entity(type);
}
base.OnModelCreating(builder);
}
}
But when a table is created after the dbcontext is created, I dont know how to add new dbset yet, because the OnModelCreating only run 1 time.
The question: How do I add new dbsets to an instance of dbcontext after its created?
OnModelCreating run only 1 time (when it first initialized) because of performance overhead.
There is one way, to bypass this, by using "Model Customizer"
First, you need some tweaking in OnConfiguring (you need to override basic implementation)
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
var serviceCollection = new ServiceCollection()
.AddEntityFrameworkSqlServer();
serviceCollection = serviceCollection.AddSingleton<IModelCustomizer, YourModelCustomizer>();
var serviceProvider = serviceCollection.BuildServiceProvider();
optionsBuilder
.UseInternalServiceProvider(serviceProvider);
}
And your Customizer should look like
public class YourModelCustomizer : ModelCustomizer
{
public override void Customize(ModelBuilder modelBuilder, DbContext dbContext)
{
base.Customize(modelBuilder, dbContext);
var entityTypeBuilderCart = modelBuilder.Entity<Models.Cart>()
.ToTable("ABC");
entityTypeBuilderCart.Property(a => a.UserId).HasColumnName("XYZ");
entityTypeBuilderCart.Property(a => a.ContractorId).HasColumnName("DFG");
entityTypeBuilderCart.Ignore(a => a.CompanyId);
var entityTypeBuilderCartArticle = modelBuilder.Entity<Models.CartArticle>()
.ToTable("IJK");
entityTypeBuilderCartArticle.Property(a => a.UserId).HasColumnName("QWE");
}
public YourModelCustomizer(ModelCustomizerDependencies dependencies) : base(dependencies)
{
}
}
I hope it will help you.
Be aware that this kind of configuration may cause performance issue.
This code works in EF Core 2.x, in EF 3.x may be some changes, and this code might need some changes.

EF Core HasQueryFilter works for only the first value in filter expression

I am using EF Core HasQueryFilter extension method, which is inside the OnModelCreating method.
I am injecting the user id into the DbContext using a service and then applying the userId to the query filter. For the first time when the OnModelCreating is executed it works fine as expected. But when I change the user and pass a different userId to the DbContext then query filter is not affected as obvious because the OnModelCreating is not called this time.
A little background of the App: It's a core 2.2 API project which authenticates users using the JWT token. I populate the user claims and initialize the injected auth service using the JWT, so for every call to the API the userId can be different hence query filter should work on different userIds.
Example codes below:
public class SqlContext : DbContext
{
private readonly IAuthService _authService;
public SqlContext(DbContextOptions options, IAuthService authService) : base(options)
{
_authService = authService;
}
public DbSet<Device> Devices { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Device>().HasQueryFilter(p => !p.IsDeleted && p.ManufacturerId == _authService.ManufacturerId);
}
}
How the DbContext is initialized.
services.AddDbContextPool<TContext>(o =>
o.UseSqlServer(configuration["Settings:SqlServer:DefaultConnection"],
b =>
{
b.MigrationsAssembly(configuration["Settings:SqlServer:MigrationAssembly"]);
b.CommandTimeout(60);
b.EnableRetryOnFailure(2);
})
.ConfigureWarnings(warnings =>
{
warnings.Throw(RelationalEventId.QueryClientEvaluationWarning);
}))
.AddTransient<TContext>();
Finally solved it.
As the filter was working, but it was not getting updated once the model was created after first request. The reason was that EF was caching the created model. So, I had to implement the IModelCacheKeyFactory in order to capture the different models as per the filters.
internal class DynamicModelCacheKeyFactory : IModelCacheKeyFactory
{
public object Create(DbContext context)
{
if (context is SqlContext dynamicContext)
{
return (context.GetType(), dynamicContext._roleCategory);
}
return context.GetType();
}
}
And attached it to the context like this.
protected override void OnConfiguring(DbContextOptionsBuilder builder)
{
base.OnConfiguring(builder);
builder.ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>();
}

Dynamically changing schema in Entity Framework Core

UPD here is the way I solved the problem. Although it's likely to be not the best one, it worked for me.
I have an issue with working with EF Core. I want to separate data for different companies in my project's database via schema-mechanism. My question is how I can change the schema name in runtime? I've found similar question about this issue but it's still unanswered and I have some different conditions. So I have the Resolve method that grants the db-context when necessary
public static void Resolve(IServiceCollection services) {
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<DomainDbContext>()
.AddDefaultTokenProviders();
services.AddTransient<IOrderProvider, OrderProvider>();
...
}
I can set the schema-name in OnModelCreating, but, as was found before, this method is called just once, so I can set schema name globaly like that
protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.HasDefaultSchema("public");
base.OnModelCreating(modelBuilder);
}
or right in the model via an attribute
[Table("order", Schema = "public")]
public class Order{...}
But how can I change the schema name on runtime? I create the context per each request, but first I fugure out the schema-name of the user via a request to a schema-shared table in the database. So what is the right way to organize that mechanism:
Figure out the schema name by the user credentials;
Get user-specific data from database from specific schema.
Thank you.
P.S. I use PostgreSql and this is the reason for lowecased table names.
Did you already use EntityTypeConfiguration in EF6?
I think the solution would be use mapping for entities on OnModelCreating method in DbContext class, something like this:
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal;
using Microsoft.Extensions.Options;
namespace AdventureWorksAPI.Models
{
public class AdventureWorksDbContext : Microsoft.EntityFrameworkCore.DbContext
{
public AdventureWorksDbContext(IOptions<AppSettings> appSettings)
{
ConnectionString = appSettings.Value.ConnectionString;
}
public String ConnectionString { get; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(ConnectionString);
// this block forces map method invoke for each instance
var builder = new ModelBuilder(new CoreConventionSetBuilder().CreateConventionSet());
OnModelCreating(builder);
optionsBuilder.UseModel(builder.Model);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.MapProduct();
base.OnModelCreating(modelBuilder);
}
}
}
The code on OnConfiguring method forces the execution of MapProduct on each instance creation for DbContext class.
Definition of MapProduct method:
using System;
using Microsoft.EntityFrameworkCore;
namespace AdventureWorksAPI.Models
{
public static class ProductMap
{
public static ModelBuilder MapProduct(this ModelBuilder modelBuilder, String schema)
{
var entity = modelBuilder.Entity<Product>();
entity.ToTable("Product", schema);
entity.HasKey(p => new { p.ProductID });
entity.Property(p => p.ProductID).UseSqlServerIdentityColumn();
return modelBuilder;
}
}
}
As you can see above, there is a line to set schema and name for table, you can send schema name for one constructor in DbContext or something like that.
Please don't use magic strings, you can create a class with all available schemas, for example:
using System;
public class Schemas
{
public const String HumanResources = "HumanResources";
public const String Production = "Production";
public const String Sales = "Sales";
}
For create your DbContext with specific schema you can write this:
var humanResourcesDbContext = new AdventureWorksDbContext(Schemas.HumanResources);
var productionDbContext = new AdventureWorksDbContext(Schemas.Production);
Obviously you should to set schema name according schema's name parameter's value:
entity.ToTable("Product", schemaName);
Define your context and pass the schema to the constructor.
In OnModelCreating Set the default schema.
public class MyContext : DbContext , IDbContextSchema
{
private readonly string _connectionString;
public string Schema {get;}
public MyContext(string connectionString, string schema)
{
_connectionString = connectionString;
Schema = schema;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.ReplaceService<IModelCacheKeyFactory, DbSchemaAwareModelCacheKeyFactory>();
optionsBuilder.UseSqlServer(_connectionString);
}
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema(Schema);
// ... model definition ...
}
}
Implement your IModelCacheKeyFactory.
public class DbSchemaAwareModelCacheKeyFactory : IModelCacheKeyFactory
{
public object Create(DbContext context)
{
return new {
Type = context.GetType(),
Schema = context is IDbContextSchema schema
? schema.Schema
: null
};
}
}
In OnConfiguring replace the default implementation of IModelCacheKeyFactory with your custom implementation.
With the default implementation of IModelCacheKeyFactory the method OnModelCreating is executed only the first time the context is instantiated and then the result is cached.
Changing the implementation you can modify how the result of OnModelCreating is cached and retrieve. Including the schema in the caching key you can get the OnModelCreating executed and cached for every different schema string passed to the context constructor.
// Get a context referring SCHEMA1
var context1 = new MyContext(connectionString, "SCHEMA1");
// Get another context referring SCHEMA2
var context2 = new MyContext(connectionString, "SCHEMA2");
Sorry everybody, I should've posted my solution before, but for some reason I didn't, so here it is.
BUT
Keep in mind that anything could be wrong with the solution since it neither hasn't been reviewed by anybody nor production-proved, probably I'll get some feedback here.
In the project I used ASP .NET Core 1
About my db structure. I have 2 contexts. The first one contains information about users (including the db scheme they should address), the second one contains user-specific data.
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>();
Notice UseInternalServiceProvider part, it was suggested by Nero Sule with the following explanation
At the very end of EFC 1 release cycle, the EF team decided to remove EF's services from the default service collection (AddEntityFramework().AddDbContext()), which means that the services are resolved using EF's own service provider rather than the application service provider.
To force EF to use your application's service provider instead, you need to first add EF's services together with the data provider to your service collection, and then configure DBContext to use 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 the context with user-specific 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 have to slightly change the context itself to make it schema-aware:
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 shared context is strictly bound to 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 getting users schema name. And yes, I know how far from the perfect code it is.
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 haven't missed anything, that's it. Now in every request by an authenticated user the proper context will be used.
I hope it helps.
There are a couple ways to do this:
Build the model externally and pass it in via DbContextOptionsBuilder.UseModel()
Replace the IModelCacheKeyFactory service with one that takes the schema into account
Took several hours to figure this out with EFCore. Seems to be alot of confusion on the proper way of implementing this. I believe the simple and correct way of handling custom models in EFCore is replacing the default IModelCacheKeyFactory service like I show below. In my example I am setting custom table names.
Create a ModelCacheKey variable in your context class.
In your context constructor, set the ModelCacheKey variable
Create a class that inherits from IModelCacheKeyFactory and use ModelCacheKey (MyModelCacheKeyFactory)
In OnConfiguring method (MyContext), replace the default IModelCacheKeyFactory
In OnModelCreating method (MyContext), use the ModelBuilder to define whatever you need.
public class MyModelCacheKeyFactory : IModelCacheKeyFactory
{
public object Create(DbContext context)
=> context is MyContext myContext ?
(context.GetType(), myContext.ModelCacheKey) :
(object)context.GetType();
}
public partial class MyContext : DbContext
{
public string Company { get; }
public string ModelCacheKey { get; }
public MyContext(string connectionString, string company) : base(connectionString)
{
Company = company;
ModelCacheKey = company; //the identifier for the model this instance will use
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
//This will create one model cache per key
optionsBuilder.ReplaceService<IModelCacheKeyFactory, MyModelCacheKeyFactory();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>(entity =>
{
//regular entity mapping
});
SetCustomConfigurations(modelBuilder);
}
public void SetCustomConfigurations(ModelBuilder modelBuilder)
{
//Here you will set the schema.
//In my example I am setting custom table name Order_CompanyX
var entityType = typeof(Order);
var tableName = entityType.Name + "_" + this.Company;
var mutableEntityType = modelBuilder.Model.GetOrAddEntityType(entityType);
mutableEntityType.RemoveAnnotation("Relational:TableName");
mutableEntityType.AddAnnotation("Relational:TableName", tableName);
}
}
The result is each instance of your context will cause efcore to cache based on the ModelCacheKey variable.
I find this blog might be useful for you. Perfect !:)
https://romiller.com/2011/05/23/ef-4-1-multi-tenant-with-code-first/
This blog is based on ef4, I'm not sure whether it will work fine with ef core.
public class ContactContext : DbContext
{
private ContactContext(DbConnection connection, DbCompiledModel model)
: base(connection, model, contextOwnsConnection: false)
{ }
public DbSet<Person> People { get; set; }
public DbSet<ContactInfo> ContactInfo { get; set; }
private static ConcurrentDictionary<Tuple<string, string>, DbCompiledModel> modelCache
= new ConcurrentDictionary<Tuple<string, string>, DbCompiledModel>();
/// <summary>
/// Creates a context that will access the specified tenant
/// </summary>
public static ContactContext Create(string tenantSchema, DbConnection connection)
{
var compiledModel = modelCache.GetOrAdd(
Tuple.Create(connection.ConnectionString, tenantSchema),
t =>
{
var builder = new DbModelBuilder();
builder.Conventions.Remove<IncludeMetadataConvention>();
builder.Entity<Person>().ToTable("Person", tenantSchema);
builder.Entity<ContactInfo>().ToTable("ContactInfo", tenantSchema);
var model = builder.Build(connection);
return model.Compile();
});
return new ContactContext(connection, compiledModel);
}
/// <summary>
/// Creates the database and/or tables for a new tenant
/// </summary>
public static void ProvisionTenant(string tenantSchema, DbConnection connection)
{
using (var ctx = Create(tenantSchema, connection))
{
if (!ctx.Database.Exists())
{
ctx.Database.Create();
}
else
{
var createScript = ((IObjectContextAdapter)ctx).ObjectContext.CreateDatabaseScript();
ctx.Database.ExecuteSqlCommand(createScript);
}
}
}
}
The main idea of these codes is to provide a static method to create different DbContext by different schema and cache them with certain identifiers.
You can use Table attribute on the fixed schema tables.
You can't use attribute on schema changing tables and you need to configure that via ToTable fluent API.
If you disable the model cache (or you write your own cache), the schema can change on every request so on the context creation (every time) you can to specify the schema.
This is the base idea
class MyContext : DbContext
{
public string Schema { get; private set; }
public MyContext(string schema) : base()
{
}
// Your DbSets here
DbSet<Emp> Emps { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Emp>()
.ToTable("Emps", Schema);
}
}
Now, you can have some different ways to determine the schema name before creating the context.
For example you can have your "system tables" on a different context so on every request you retrieve the schema name from the user name using the system tables and than create the working context on the right schema (you can share tables between contexts).
You can have your system tables detached from the context and use ADO .Net to access to them.
Probably there are several other solutions.
You can also have a look here
Multi-Tenant With Code First EF6
and you can google ef multi tenant
EDIT
There is also the problem of the model caching (I forgot about that).
You have to disable the model caching or change the behavior of the cache.
maybe I'm a bit late to this answer
my problem was handling different schema with the same structure lets say multi-tenant.
When I tried to create different instances of the same context for the different schemas, Entity frameworks 6 comes to play, catching the first time the dbContext was created then for the following instances they were creates with a different schemas name but onModelCreating were never called meaning that each instance was pointing to the same previously catched Pre-Generated Views, pointing to the first schema.
Then I realized that creating new classes inheriting from myDBContext one for each schema will solve my problem by overcoming entity Framework catching problem creating one new fresh context for each schema, but then comes the problem that we will end with hardcoded schemas, causing another problem in terms of code scalability when we need to add another schema, having to add more classes and recompile and publish a new version of the application.
So I decided to go a little further creating, compiling and adding the classes to the current solution in runtime.
Here is the code
public static MyBaseContext CreateContext(string schema)
{
MyBaseContext instance = null;
try
{
string code = $#"
namespace MyNamespace
{{
using System.Collections.Generic;
using System.Data.Entity;
public partial class {schema}Context : MyBaseContext
{{
public {schema}Context(string SCHEMA) : base(SCHEMA)
{{
}}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{{
base.OnModelCreating(modelBuilder);
}}
}}
}}
";
CompilerParameters dynamicParams = new CompilerParameters();
Assembly currentAssembly = Assembly.GetExecutingAssembly();
dynamicParams.ReferencedAssemblies.Add(currentAssembly.Location); // Reference the current assembly from within dynamic one
// Dependent Assemblies of the above will also be needed
dynamicParams.ReferencedAssemblies.AddRange(
(from holdAssembly in currentAssembly.GetReferencedAssemblies()
select Assembly.ReflectionOnlyLoad(holdAssembly.FullName).Location).ToArray());
// Everything below here is unchanged from the previous
CodeDomProvider dynamicLoad = CodeDomProvider.CreateProvider("C#");
CompilerResults dynamicResults = dynamicLoad.CompileAssemblyFromSource(dynamicParams, code);
if (!dynamicResults.Errors.HasErrors)
{
Type myDynamicType = dynamicResults.CompiledAssembly.GetType($"MyNamespace.{schema}Context");
Object[] args = { schema };
instance = (MyBaseContext)Activator.CreateInstance(myDynamicType, args);
}
else
{
Console.WriteLine("Failed to load dynamic assembly" + dynamicResults.Errors[0].ErrorText);
}
}
catch (Exception ex)
{
string message = ex.Message;
}
return instance;
}
I hope this help someone to save some time.
Update for MVC Core 2.1
You can create a model from a database with multiple schemas. The system is a bit schema-agnostic in its naming. Same named tables get a "1" appended. "dbo" is the assumed schema so you don't add anything by prefixing a table name with it the PM command
You will have to rename model file names and class names yourself.
In the PM console
Scaffold-DbContext "Data Source=localhost;Initial Catalog=YourDatabase;Integrated Security=True" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -force -Tables TableA, Schema1.TableA
I actually found it to be a simpler solution with an EF interceptor.
I actually keep the onModeling method:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("dbo"); // this is important to always be dbo
// ... model definition ...
}
And this code will be in Startup:
public void ConfigureServices(IServiceCollection services)
{
// if I add a service I can have the lambda (factory method) to read from request the schema (I put it in a cookie)
services.AddScoped<ISchemeInterceptor, SchemeInterceptor>(provider =>
{
var context = provider.GetService<IHttpContextAccessor>().HttpContext;
var scheme = "dbo";
if (context.Request.Cookies["schema"] != null)
{
scheme = context.Request.Cookies["schema"];
}
return new SchemeInterceptor(scheme);
});
services.AddDbContext<MyContext>(options =>
{
var sp = services.BuildServiceProvider();
var interceptor = sp.GetService<ISchemeInterceptor>();
options.UseSqlServer(Configuration.GetConnectionString("Default"))
.AddInterceptors(interceptor);
});
And the interceptor code looks something like this (but basically we use ReplaceSchema):
public interface ISchemeInterceptor : IDbCommandInterceptor
{
}
public class SchemeInterceptor : DbCommandInterceptor, ISchemeInterceptor
{
private readonly string _schema;
public SchemeInterceptor(string schema)
{
_schema = schema;
}
public override Task<InterceptionResult<object>> ScalarExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<object> result,
CancellationToken cancellationToken = new CancellationToken())
{
ReplaceSchema(command);
return base.ScalarExecutingAsync(command, eventData, result, cancellationToken);
}
public override InterceptionResult<object> ScalarExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<object> result)
{
ReplaceSchema(command);
return base.ScalarExecuting(command, eventData, result);
}
public override Task<InterceptionResult<int>> NonQueryExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<int> result,
CancellationToken cancellationToken = new CancellationToken())
{
ReplaceSchema(command);
return base.NonQueryExecutingAsync(command, eventData, result, cancellationToken);
}
public override InterceptionResult<int> NonQueryExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<int> result)
{
ReplaceSchema(command);
return base.NonQueryExecuting(command, eventData, result);
}
public override InterceptionResult<DbDataReader> ReaderExecuting(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result)
{
ReplaceSchema(command);
return result;
}
public override Task<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result,
CancellationToken cancellationToken = new CancellationToken())
{
ReplaceSchema(command);
return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
}
private void ReplaceSchema(DbCommand command)
{
command.CommandText = command.CommandText.Replace("[dbo]", $"[{_schema}]");
}
public override void CommandFailed(DbCommand command, CommandErrorEventData eventData)
{
// here you can handle cases like schema not found
base.CommandFailed(command, eventData);
}
public override Task CommandFailedAsync(DbCommand command, CommandErrorEventData eventData,
CancellationToken cancellationToken = new CancellationToken())
{
// here you can handle cases like schema not found
return base.CommandFailedAsync(command, eventData, cancellationToken);
}
}
If the only difference between databases is schema name the simplest way to get rid of the problem is to remove line of code which is setting the default schema in OnModelCreating method:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
...
modelBuilder.HasDefaultSchema("YourSchemaName"); <-- remove or comment this line
...
}
In this case underneeth sql queries run by EF Core won't contain schema name in their FROM clause. Then you will be able to write a method which will set correct DbContext depending on your custom conditions.
Here is an example which I used to connect to different Oracle databases with the same database structure (in short let's say that in Oracle schema is the same as user). If you're using another DB you just need to put correct connection string and then modify it.
private YourDbContext SetDbContext()
{
string connStr = #"Data Source=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=server_ip)(PORT=1521)))(CONNECT_DATA=(SID = server_sid)));User Id=server_user ;Password=server_password";
//You can get db connection details e.g. from app config
List<string> connections = config.GetSection("DbConneections");
string serverIp;
string dbSid;
string dBUser;
string dbPassword;
/* some logic to choose a connection from config and set up string variables for a connection*/
connStr = connStr.Replace("server_ip", serverIp);
connStr = connStr.Replace("server_sid", dbSid);
connStr = connStr.Replace("server_user", dBUser);
connStr = connStr.Replace("server_password", dbPassword);
var dbContext = dbContextFactory.CreateDbContext();
dbContext.Database.CloseConnection();
dbContext.Database.SetConnectionString(connStr);
return dbContext;
}
Finally you will be able to set desired dbContext where it's needed invoking this method before, you can also pass some arguments to the method to help you choose correct db.

Categories

Resources