Динамически меняющаяся схема в ядре Entity Framework

UPD вот как я решил проблему. Хотя это, вероятно, не самый лучший, это сработало для меня.


У меня проблема с работой с EF Core. Я хочу разделить данные для разных компаний в базе данных моего проекта с помощью механизма схемы. У меня вопрос, как я могу изменить имя схемы во время выполнения? Я нашел похожий вопрос об этой проблеме, но он все еще остается без ответа, и у меня есть некоторые другие условия. Таким образом, у меня есть метод Resolve который предоставляет db-контекст, когда это необходимо

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

Я могу установить имя схемы в OnModelCreating, но, как было обнаружено ранее, этот метод вызывается только один раз, поэтому я могу установить глобальное имя схемы следующим образом

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

или прямо в модели через атрибут

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

Но как я могу изменить имя схемы во время выполнения? Я создаю контекст для каждого запроса, но сначала вычеркиваю имя схемы пользователя через запрос к общей таблице схемы в базе данных. Итак, как правильно организовать этот механизм:

  1. Выяснить имя схемы по учетным данным пользователя;
  2. Получить пользовательские данные из базы данных из конкретной схемы.

Спасибо.

PS Я использую PostgreSql, и это является причиной низких имен таблиц.

Ответ 1

Вы уже использовали EntityTypeConfiguration в EF6?

Я думаю, что решением будет использование сопоставления для объектов в методе OnModelCreating в классе DbContext, что-то вроде этого:

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

Код метода OnConfiguring заставляет выполнение MapProduct для каждого создания экземпляра для класса DbContext.

Определение метода 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;
        }
    }
}

Как вы можете видеть выше, существует строка для установки схемы и имени для таблицы, вы можете отправить имя схемы для одного конструктора в DbContext или что-то в этом роде.

Не используйте магические строки, вы можете создать класс со всеми доступными схемами, например:

using System;

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

Для создания DbContext с конкретной схемой вы можете написать это:

var humanResourcesDbContext = new AdventureWorksDbContext(Schemas.HumanResources);

var productionDbContext = new AdventureWorksDbContext(Schemas.Production);

Очевидно, вы должны установить имя схемы в соответствии с именем параметра схемы:

entity.ToTable("Product", schemaName);

Ответ 2

Есть несколько способов сделать это:

  • Создайте внешнюю модель и передайте ее через DbContextOptionsBuilder.UseModel()
  • Заменить службу IModelCacheKeyFactory той, которая учитывает схему.

Ответ 3

Я считаю, что этот блог может быть вам полезен. Отлично!:)

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

Этот блог основан на ef4, я не уверен, будет ли он работать отлично с 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);
            }
        }
    }
}

Основная идея этих кодов заключается в предоставлении статического метода для создания разных DbContext по другой схеме и кэширования их с помощью определенных идентификаторов.

Ответ 4

Извините всех, я должен был опубликовать свое решение раньше, но по какой-то причине я не сделал, так что вот оно.

НО

Имейте в виду, что с решением может быть что-то не так, поскольку оно не было никем не проверено и не доказано производственным процессом, возможно, здесь я получу отзыв.

В проекте я использовал ASP.NET Core 1


О моей структуре БД. У меня есть 2 контекста. Первый содержит информацию о пользователях (включая схему БД, к которой они должны обращаться), второй содержит данные, относящиеся к конкретному пользователю.

В Startup.cs я добавляю оба контекста

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

Обратите внимание на использование UseInternalServiceProvider, это было предложено Nero Sule со следующим объяснением

В самом конце цикла выпуска EFC 1 команда EF решила удалить службы EF из коллекции служб по умолчанию (AddEntityFramework(). AddDbContext()), что означает, что службы разрешаются с использованием собственного поставщика услуг EF, а не службы приложения. поставщик.

Чтобы заставить EF вместо этого использовать поставщика услуг приложений, сначала необходимо добавить службы EF вместе с поставщиком данных в коллекцию сервисов, а затем настроить DBContext для использования внутреннего поставщика услуг.

Теперь нам нужен 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 - это контекст с пользовательскими данными

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

Также нам нужно немного изменить сам контекст, чтобы он учитывал схему:

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

и общий контекст строго привязан к 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 отвечает за получение имени схемы пользователя. И да, я знаю, насколько это далеко от идеального кода.

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

И если я ничего не пропустил, то это. Теперь в каждом запросе аутентифицированного пользователя будет использоваться соответствующий контекст.

Я надеюсь, что это помогает.

Ответ 5

может быть, я немного опоздал на этот ответ

Моя проблема заключалась в обработке другой схемы с одинаковой структурой, скажем, мультитенантной.

Когда я попытался создать разные экземпляры одного и того же контекста для разных схем, Entity Framework 6 начинает играть, ловя первый раз, когда был создан dbContext, а затем для следующих экземпляров они были созданы с другим именем схемы, но onModelCreating никогда не назывался означающим что каждый экземпляр указывает на одни и те же предварительно сгенерированные предварительно сгенерированные представления, указывая на первую схему.

Затем я понял, что создание новых классов, унаследованных от myDBContext по одному для каждой схемы, решит мою проблему, преодолев проблему отлова Entity Framework, создав один новый свежий контекст для каждой схемы, но затем возникнет проблема, которую мы закончим с жестко закодированными схемами, что вызовет еще одну проблему в Условия масштабируемости кода, когда нам нужно добавить другую схему, нужно добавить больше классов, перекомпилировать и опубликовать новую версию приложения.

Поэтому я решил пойти немного дальше, создавая, компилируя и добавляя классы в текущее решение во время выполнения.

Вот код

public static MyBaseContext CreateContext(string schema)
{
    MyBaseContext instance = null;
    try
    {
        string code = [email protected]"
            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;
}

Я надеюсь, что это поможет кому-то сэкономить время.

Ответ 6

Вы можете использовать атрибут Table в таблицах фиксированной схемы.

Вы не можете использовать атрибут для таблиц смены схемы, и вам нужно настроить это через свободный API ToTable.
Если вы отключите кеш модели (или напишите свой собственный кеш), схема может измениться при каждом запросе, поэтому при создании контекста (каждый раз) вы можете указать схему.

Это основная идея

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

Теперь у вас могут быть разные способы определения имени схемы перед созданием контекста.
Например, вы можете иметь свои "системные таблицы" в другом контексте, так что при каждом запросе вы получите имя схемы из имени пользователя с помощью системных таблиц и создаете рабочий контекст в правильной схеме (вы можете делиться таблицами между контекстами).
Вы можете отключить системные таблицы из контекста и использовать ADO.Net для доступа к ним.
Возможно, есть еще несколько решений.

Вы также можете посмотреть здесь
Multi-Tenant с кодом First EF6

и вы можете google ef multi tenant

ИЗМЕНИТЬ
Существует также проблема кэширования модели (я забыл об этом). Вы должны отключить кэширование модели или изменить поведение кэша.

Ответ 7

Обновление для MVC Core 2.1

Вы можете создать модель из базы данных с несколькими схемами. Система немного независима от схемы в своих именах. Таблицы с одинаковыми именами добавляются в "1". "dbo" - предполагаемая схема, поэтому вы ничего не добавляете, добавляя префикс имени таблицы к ней с помощью команды PM

Вам придется самостоятельно переименовывать имена файлов моделей и классов.

В консоли PM

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