Esquema cambiante dinámicamente en Entity Framework Core

UPD aquí es la forma en que resolví el problema. Aunque es probable que no sea el mejor, funcionó para mí.


Tengo un problema con el trabajo con EF Core. Quiero separar los datos de diferentes compañías en la base de datos de mi proyecto a través del mecanismo de esquema. ¿Mi pregunta es cómo puedo cambiar el nombre del esquema en tiempo de ejecución? He encontrado una pregunta similar sobre este problema, pero todavía tengo una respuesta y tengo algunas condiciones diferentes. Así que tengo el método Resolve que otorga db-context cuando es necesario

 public static void Resolve(IServiceCollection services) { services.AddIdentity() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); services.AddTransient(); ... } 

Puedo establecer el nombre del esquema en OnModelCreating , pero, como se encontró antes, este método se llamó solo una vez, así que puedo configurar el nombre del esquema globaly aquí de esa manera

 protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasDefaultSchema("public"); base.OnModelCreating(modelBuilder); } 

o justo en el modelo a través de atributo como ese

 [Table("order", Schema = "public")] public class Order{...} 

Pero, ¿cómo puedo cambiar el nombre del esquema en tiempo de ejecución? Creo el contexto de ef para cada solicitud, pero en primer lugar fuguro el nombre del esquema para el usuario a través de una tabla de esquemas compartidos en la base de datos. Entonces, ¿cuál es la verdadera manera de organizar ese mecanismo:

  1. Averiguar el nombre del esquema por credenciales del usuario;
  2. Obtenga datos específicos del usuario de la base de datos de un esquema específico.

Gracias.

PS: uso PostgreSql y esta es una razón del nombre de tabla de bajo pronóstico.

¿Ya usaste EntityTypeConfiguration en EF6?

Creo que la solución sería usar mapeo para entidades en el método OnModelCreating en la clase DbContext, algo como esto:

 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) { 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); } } } 

El código en el método OnConfiguring obliga a la ejecución de MapProduct en cada creación de instancia para la clase DbContext.

Definición del método MapProduct:

 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(); entity.ToTable("Product", schema); entity.HasKey(p => new { p.ProductID }); entity.Property(p => p.ProductID).UseSqlServerIdentityColumn(); return modelBuilder; } } } 

Como puede ver arriba, hay una línea para configurar el esquema y el nombre de la tabla, puede enviar el nombre del esquema para un constructor en DbContext o algo así.

No use cadenas mágicas, puede crear una clase con todos los esquemas disponibles, por ejemplo:

 using System; public class Schemas { public const String HumanResources = "HumanResources"; public const String Production = "Production"; public const String Sales = "Production"; } 

Para crear su DbContext con un esquema específico puede escribir esto:

 var humanResourcesDbContext = new AdventureWorksDbContext(Schemas.HumanResources); var productionDbContext = new AdventureWorksDbContext(Schemas.Production); 

Obviamente, debe establecer el nombre del esquema de acuerdo con el valor del parámetro del nombre del esquema:

 entity.ToTable("Product", schemaName); 

Hay un par de maneras de hacer esto:

  • Construya el modelo externamente y páselo a través de DbContextOptionsBuilder.UseModel()
  • Reemplace el servicio IModelCacheKeyFactory por uno que tenga en cuenta el esquema

Me parece que este blog puede ser útil para ti. Perfecto !:)

https://romiller.com/2011/05/23/ef-4-1-multi-tenant-with-code-first/

Este blog se basa en ef4, no estoy seguro de si funcionará bien con ef core.

 public class ContactContext : DbContext { private ContactContext(DbConnection connection, DbCompiledModel model) : base(connection, model, contextOwnsConnection: false) { } public DbSet People { get; set; } public DbSet ContactInfo { get; set; } private static ConcurrentDictionary, DbCompiledModel> modelCache = new ConcurrentDictionary, DbCompiledModel>(); ///  /// Creates a context that will access the specified tenant ///  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(); builder.Entity().ToTable("Person", tenantSchema); builder.Entity().ToTable("ContactInfo", tenantSchema); var model = builder.Build(connection); return model.Compile(); }); return new ContactContext(connection, compiledModel); } ///  /// Creates the database and/or tables for a new tenant ///  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); } } } } 

La idea principal de estos códigos es proporcionar un método estático para crear diferentes DbContext por diferentes esquemas y almacenarlos en caché con ciertos identificadores.

Puede usar el atributo Tabla en las tablas de esquema fijo.

No puede usar el atributo en las tablas de cambio de esquema y necesita configurarlo a través de la API fluida de ToTable.
Si deshabilita la memoria caché del modelo (o escribe su propia memoria caché), el esquema puede cambiar en cada solicitud, por lo que en la creación de contexto (cada vez) puede especificar el esquema.

Esta es la idea base.

 class MyContext : DbContext { public string Schema { get; private set; } public MyContext(string schema) : base() { } // Your DbSets here DbSet Emps { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity() .ToTable("Emps", Schema); } } 

Ahora, puede tener algunas formas diferentes de determinar el nombre del esquema antes de crear el contexto.
Por ejemplo, puede tener sus “tablas del sistema” en un contexto diferente, de modo que en cada solicitud recupere el nombre del esquema del nombre de usuario utilizando las tablas del sistema y luego cree el contexto de trabajo en el esquema correcto (puede compartir tablas entre contextos).
Puede tener las tablas de su sistema separadas del contexto y usar ADO .Net para acceder a ellas.
Probablemente hay varias otras soluciones.

También puedes echar un vistazo aquí.
Multi-inquilino con el código primero EF6

y usted puede ef multi tenant Google ef multi tenant

EDITAR
También está el problema del almacenamiento en caché del modelo (me olvidé de eso). Debe deshabilitar la caché del modelo o cambiar el comportamiento de la caché.

Disculpe a todos, debería haber publicado mi solución antes, pero por alguna razón no lo hice, así que aquí está.

PERO

Tenga en cuenta que cualquier cosa podría estar mal con la solución ya que no ha sido revisada por nadie ni probada en la producción, probablemente obtendré algunos comentarios aquí.

En el proyecto utilicé ASP .NET Core 1.


Acerca de mi estructura de db. Tengo 2 contextos. El primero contiene información sobre los usuarios (incluido el esquema de db que deben abordar), el segundo contiene datos específicos del usuario.

En Startup.cs agrego ambos contextos.

 public void ConfigureServices(IServiceCollection services.AddEntityFrameworkNpgsql() .AddDbContext(options => options.UseNpgsql(Configuration["MasterConnection"])) .AddDbContext((serviceProvider, options) => options.UseNpgsql(Configuration["MasterConnection"]) .UseInternalServiceProvider(serviceProvider)); ... services.Replace(ServiceDescriptor.Singleton()); services.TryAddSingleton(); 

Tenga en UseInternalServiceProvider parte de uso de UseInternalServiceProvider , fue sugerida por Nero Sule con la siguiente explicación

Al final del ciclo de lanzamiento de EFC 1, el equipo de EF decidió eliminar los servicios de EF de la colección de servicios predeterminada (AddEntityFramework (). AddDbContext ()), lo que significa que los servicios se resuelven utilizando el propio proveedor de servicios de EF en lugar del servicio de la aplicación proveedor.

Para forzar a EF a usar el proveedor de servicios de su aplicación, primero debe agregar los servicios de EF junto con el proveedor de datos a su colección de servicios, y luego configurar DBContext para usar el proveedor de servicios interno

Ahora necesitamos 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); } } 

donde DomainDbContext es el contexto con datos específicos del usuario

 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(); } } 

También tenemos que cambiar ligeramente el contexto en sí mismo para que tenga en cuenta el esquema:

 public class DomainDbContext : IdentityDbContext { public readonly string SchemaName; public DbSet Foos{ get; set; } public DomainDbContext(ICompanyProvider companyProvider, DbContextOptions options) : base(options) { SchemaName = companyProvider.GetSchemaName(); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasDefaultSchema(SchemaName); base.OnModelCreating(modelBuilder); } } 

y el contexto compartido está estrictamente vinculado al esquema shared :

 public class SharedDbContext : IdentityDbContext { private const string SharedSchemaName = "shared"; public DbSet Foos{ get; set; } public SharedDbContext(DbContextOptions options) : base(options) {} protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasDefaultSchema(SharedSchemaName); base.OnModelCreating(modelBuilder); } } 

ICompanyProvider es responsable de obtener el nombre del esquema de los usuarios. Y sí, sé a qué distancia está el código perfecto.

 public interface ICompanyProvider { string GetSchemaName(); } public class CompanyProvider : ICompanyProvider { private readonly SharedDbContext _context; private readonly IHttpContextAccessor _accesor; private readonly UserManager _userManager; public CompanyProvider(SharedDbContext context, IHttpContextAccessor accesor, UserManager userManager) { _context = context; _accesor = accesor; _userManager = userManager; } public string GetSchemaName() { Task 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; } } 

Y si no me he perdido nada, ya está. Ahora, en cada solicitud de un usuario autenticado, se utilizará el contexto adecuado.

Espero que ayude.

tal vez llego un poco tarde a esta respuesta

mi problema fue manejar diferentes esquemas con la misma estructura, digamos multi-tenant.

Cuando intenté crear diferentes instancias del mismo contexto para los diferentes esquemas, Entity frameworks 6 se puso a jugar, capturando la primera vez que se creó el dbContext y luego las siguientes instancias fueron creadas con un nombre de esquemas diferente, pero nunca se llamó significado a onModelCreating que cada instancia estaba apuntando a las mismas Vistas Pre-Generadas previamente capturadas, apuntando al primer esquema.

Luego me di cuenta de que crear nuevas clases heredadas de myDBContext para cada esquema resolverá mi problema al superar el problema de captura de Entity Framework creando un nuevo contexto nuevo para cada esquema, pero luego viene el problema de que terminaremos con esquemas codificados, causando otro problema en términos de escalabilidad de código cuando necesitamos agregar otro esquema, tener que agregar más clases y volver a comstackr y publicar una nueva versión de la aplicación.

Así que decidí ir un poco más lejos creando, comstackndo y agregando las clases a la solución actual en tiempo de ejecución.

Aquí está el código

 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; } 

Espero que esto ayude a alguien a ahorrar tiempo.

Actualización para MVC Core 2.1

Puede crear un modelo a partir de una base de datos con múltiples esquemas. El sistema es un poco esquemático en su denominación. Las mismas tablas nombradas obtienen un “1” anexado. “dbo” es el esquema asumido, por lo que no agrega nada prefijando un nombre de tabla con el comando PM

Tendrá que cambiar el nombre de los nombres de archivo de modelo y los nombres de clase.

En la consola de pm

 Scaffold-DbContext "Data Source=localhost;Initial Catalog=YourDatabase;Integrated Security=True" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -force -Tables TableA, Schema1.TableA