web-dev-qa-db-fra.com

Modification dynamique du schéma dans Entity Framework Core

UPDhere est la façon dont j'ai résolu le problème. Bien que ce ne soit probablement pas le meilleur, cela a fonctionné pour moi.


J'ai un problème avec EF Core. Je souhaite séparer les données de différentes sociétés dans la base de données de mon projet via un mécanisme de schéma. Ma question est la suivante: comment puis-je modifier le nom du schéma au moment de l'exécution? J'ai trouvé une question similaire à propos de ce problème mais il reste encore une réponse et j'ai des conditions différentes. J'ai donc la méthode Resolve qui accorde db-context en cas de besoin

public static void Resolve(IServiceCollection services) {
    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<DomainDbContext>()
        .AddDefaultTokenProviders();
    services.AddTransient<IOrderProvider, OrderProvider>();
    ...
}

Je peux définir le nom du schéma dans OnModelCreating, mais, comme cela a été trouvé auparavant, cette méthode est appelée une seule fois et je peux donc définir le nom du schéma globalement comme ceci.

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

ou droit dans le modèle via attribut comme ça

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

Mais comment puis-je changer le nom du schéma en exécution? Je crée le contexte de ef pour chaque demande, mais tout d’abord, je lance un nom de schéma pour l’utilisateur via une requête vers une table partagée dans la base de données. Alors, quelle est la vraie façon d'organiser ce mécanisme:

  1. Déterminez le nom du schéma à l'aide des informations d'identification de l'utilisateur.
  2. Obtenir des données spécifiques à l'utilisateur de la base de données à partir d'un schéma spécifique.

Je vous remercie.

P.S. J'utilise PostgreSql et ceci est une raison du nom de table lowecased.

9
user3272018

Avez-vous déjà utilisé EntityTypeConfiguration dans EF6?

Je pense que la solution serait d'utiliser le mappage pour les entités sur la méthode OnModelCreating dans la classe DbContext, quelque chose comme ceci:

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

Le code de la méthode OnConfiguring force l'exécution de MapProduct à chaque création d'instance pour la classe DbContext.

Définition de la méthode 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<Product>();

            entity.ToTable("Product", schema);

            entity.HasKey(p => new { p.ProductID });

            entity.Property(p => p.ProductID).UseSqlServerIdentityColumn();

            return modelBuilder;
        }
    }
}

Comme vous pouvez le voir ci-dessus, il existe une ligne pour définir le schéma et le nom de la table. Vous pouvez envoyer un nom de schéma pour un constructeur dans DbContext ou quelque chose du genre.

N'utilisez pas de chaînes magiques, vous pouvez créer une classe avec tous les schémas disponibles, par exemple:

using System;

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

Pour créer votre DbContext avec un schéma spécifique, vous pouvez écrire ceci:

var humanResourcesDbContext = new AdventureWorksDbContext(Schemas.HumanResources);

var productionDbContext = new AdventureWorksDbContext(Schemas.Production);

Vous devez évidemment définir le nom du schéma en fonction de la valeur du paramètre name du schéma:

entity.ToTable("Product", schemaName);
9
H. Herzl

Il y a plusieurs façons de le faire:

  • Construisez le modèle en externe et transmettez-le via DbContextOptionsBuilder.UseModel()
  • Remplacez le service IModelCacheKeyFactory par un service prenant en compte le schéma
4
bricelam

Je trouve que ce blog pourrait vous être utile. Parfait! :)

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

Ce blog est basé sur ef4, je ne sais pas si cela fonctionnera correctement avec 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);
            }
        }
    }
}

L'idée principale de ces codes est de fournir une méthode statique pour créer différents DbContext par différents schémas et les mettre en cache avec certains identificateurs.

3
snys98

Désolé tout le monde, j'aurais dû poster ma solution auparavant, mais pour une raison quelconque je ne l'ai pas fait, alors la voici.

MAIS

Gardez à l'esprit que la solution peut poser problème, car elle n'a été vérifiée par personne ni prouvée par la production, je vais probablement obtenir quelques informations ici.

Dans le projet, j'ai utilisé ASP .NET Core 1} _


A propos de ma structure de base de données. J'ai 2 contextes. Le premier contient des informations sur les utilisateurs (y compris le schéma de base de données qu’ils doivent traiter), le second contient des données spécifiques à l’utilisateur.

Dans Startup.cs j'ajoute les deux contextes

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

Remarquez que la partie UseInternalServiceProvider a été suggérée par Nero Sule avec l'explication suivante

À la fin du cycle de publication d'EFC 1, l'équipe EF a décidé de supprimer les services d'EF de la collection de services par défaut (AddEntityFramework (). AddDbContext ()), ce qui signifie que les services sont résolus à l'aide du propre fournisseur de services d'EF plutôt que du service d'application. fournisseur.

Pour forcer EF à utiliser le fournisseur de services de votre application, vous devez d'abord ajouter les services de EF ainsi que le fournisseur de données à votre collection de services, puis configurer DBContext pour qu'il utilise un fournisseur de services interne.

Maintenant nous avons besoin de 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);
    }
}

DomainDbContext est le contexte avec les données spécifiques à l'utilisateur

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

Nous devons également modifier légèrement le contexte lui-même pour le rendre compatible avec les schémas:

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

et le contexte partagé est strictement lié au schéma shared:

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 est responsable de l'obtention du nom de schéma des utilisateurs. Et oui, je sais à quel point il est loin du code parfait.

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

Et si je n'ai rien manqué, c'est tout. Maintenant, dans chaque demande d'un utilisateur authentifié, le contexte approprié sera utilisé.

J'espère que ça aide.

2
user3272018

Vous pouvez utiliser l'attribut Table sur les tables de schéma fixes. 

Vous ne pouvez pas utiliser d'attribut sur les tables de changement de schéma et vous devez le configurer via l'API ToTable fluent.
Si vous désactivez le cache du modèle (ou si vous écrivez votre propre cache), le schéma peut changer à chaque demande, donc vous pouvez spécifier le schéma lors de la création du contexte (à chaque fois).

C'est l'idée de base

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

Maintenant, vous pouvez utiliser différentes méthodes pour déterminer le nom du schéma avant de créer le contexte.
Par exemple, vous pouvez avoir vos "tables système" dans un contexte différent. Ainsi, à chaque requête, vous récupérez le nom du schéma à partir du nom d'utilisateur à l'aide des tables système et créez le contexte de travail sur le schéma de droite contextes).
Vous pouvez séparer vos tables système du contexte et utiliser ADO .Net pour y accéder.
Il existe probablement plusieurs autres solutions.

Vous pouvez aussi jeter un oeil ici
Multi-locataire avec code First EF6

et vous pouvez google ef multi tenant

MODIFIER
Il y a aussi le problème de la mise en cache du modèle (je l'avais oublié) . Vous devez désactiver la mise en cache du modèle ou modifier le comportement du cache.

1
bubi

peut-être que je suis un peu en retard à cette réponse

mon problème était de traiter différents schémas avec la même structure, disons multi-locataires.

Quand j'ai essayé de créer différentes instances du même contexte pour les différents schémas, les frameworks Entity 6 se mettent en marche. La première fois que dbContext a été créé, ils ont été créés avec un nom de schéma différent, mais onModelCreating n'a jamais été appelé que chaque instance pointait vers les mêmes vues générées précédemment capturées, pointant vers le premier schéma.

Ensuite, je me suis rendu compte que la création de nouvelles classes héritant de myDBContext, une résolution pour chaque schéma résoudrait mon problème en surmontant le problème de capture d'entités Framework, créant ainsi un nouveau contexte pour chaque schéma. termes d’évolutivité du code lorsque nous devons ajouter un autre schéma, devoir ajouter plus de classes, recompiler et publier une nouvelle version de l’application.

J'ai donc décidé d'aller un peu plus loin dans la création, la compilation et l'ajout des classes à la solution actuelle au moment de l'exécution.

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

J'espère que cela aidera quelqu'un à gagner du temps.

0
Randy

Mise à jour pour MVC Core 2.1

Vous pouvez créer un modèle à partir d'une base de données avec plusieurs schémas. Le système est un peu agnostique au schéma dans sa dénomination. Les tables portant le même nom reçoivent un "1" ajouté. "dbo" est le schéma supposé afin de ne rien ajouter en préfixant un nom de table avec la commande PM

Vous devrez renommer vous-même les noms de fichier de modèle et les noms de classe.

Dans la console PM

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