当 API 运行时,EF Core 不会更改表架构

问题描述 投票:0回答:1

我正在开发一个多租户 ASP.NET Core Web API 项目,并使用 EF Core 8 作为我的 ORM 和 SQL Server。该项目支持多个租户,我需要在运行时创建数据库并将架构从

dbo
更改为“巴西 cpf 文档编号”。

我已经准备好迁移。当项目运行并且我运行

Migrate()
时,它会正确创建数据库,但不会更改架构。 EF Core 坚持维护表
dbo
,不遵守我告知的参数。

有人知道这是什么吗?我已经尝试了所有方法,并且已经尝试了 5 个多月,但没有解决方案。

//Apply Migrations
public static void Test(this IServiceProvider serviceProvider, IConfiguration configuration, TenantRequestViewModel tenantRequestViewModel)
{
    using (var scope = serviceProvider.CreateScope())
    {
        var currentTenantRepo = scope.ServiceProvider.GetRequiredService<ICurrentTenantRepository>();

        if (!currentTenantRepo.SetTenantAsync(tenantRequestViewModel.DocumentNumber).Result) { { } }

        string dbName = DataBaseConst.DATABASE_NAME_ERP_VET + "-" + tenantRequestViewModel.DocumentNumber;
        string defaultConnectionString = configuration.GetConnectionString("DefaultConnection");
        string connectionString = currentTenantRepo.ConnectionString ?? defaultConnectionString.Replace(DataBaseConst.DATABASE_NAME_ERP_VET, dbName);

        var appDbContextOptionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
        appDbContextOptionsBuilder.UseSqlServer(connectionString);

        var mediatorHandler = scope.ServiceProvider.GetRequiredService<IMediatorHandler>();

        using (var appDbContext = new AppDbContext(appDbContextOptionsBuilder.Options, currentTenantRepo, mediatorHandler, currentTenantRepo.Schema))
        {
            Console.WriteLine($"Current Schema: {currentTenantRepo.Schema}");

            if (appDbContext.Database.GetPendingMigrations().Any())
            {
                appDbContext.Database.Migrate();
            }
        }       
    }
}

// My Context
public sealed class AppDbContext : DbContext, IUnitOfWork
{
    private readonly IMediatorHandler _mediatorHandler;

    private readonly ICurrentTenantRepository _currentTenantRepository;
    public Guid CurrentTenantId { get; set; }
    public string? CurrentTenantConnectionString { get; set; }
    public string? CurrentSchema { get; set; }

    public AppDbContext(DbContextOptions<AppDbContext> options, ICurrentTenantRepository currentTenantRepository, IMediatorHandler mediatorHandler, string? schema = null) : base(options)
    {
        _mediatorHandler = mediatorHandler;
        _currentTenantRepository = currentTenantRepository;

        if (_currentTenantRepository is not null)
        {
            CurrentTenantId = _currentTenantRepository.TenantId;
            CurrentTenantConnectionString = _currentTenantRepository.ConnectionString;
            CurrentSchema = string.IsNullOrEmpty(schema) ? _currentTenantRepository.Schema : schema ;
        }

        //ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
        //ChangeTracker.AutoDetectChangesEnabled = false;
    }

    public DbSet<Customer> Customers { get; set; }
    {
        protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Ignore<ValidationResult>();
        modelBuilder.Ignore<Event>();
        modelBuilder.Ignore<CreateTenantRequest>();
        modelBuilder.Ignore<TenantResponse>();
        modelBuilder.Ignore<Tenant>();

        modelBuilder.HasDefaultSchema(CurrentSchema);
        
        if (!string.IsNullOrEmpty(CurrentSchema))
        {
            foreach (var entityType in modelBuilder.Model.GetEntityTypes())
            {
                entityType.SetSchema(CurrentSchema);
            }              
        }           

        foreach (var property in modelBuilder.Model.GetEntityTypes().SelectMany(
            e => e.GetProperties().Where(p => p.ClrType == typeof(string))))
            property.SetColumnType("varchar(100)");
            
        //Schema name passed to table configuration
        modelBuilder.ApplyConfiguration(new CustomerMap(CurrentSchema));

        base.OnModelCreating(modelBuilder);
    }

    // On Configuring -- dynamic connection string, fires on every request
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        string? tenantConnectionString = CurrentTenantConnectionString;

        if (!string.IsNullOrEmpty(tenantConnectionString)) // use tenant db if one is specified
        {
            _ = optionsBuilder.UseSqlServer(tenantConnectionString);
        }

        optionsBuilder.ReplaceService<IModelCacheKeyFactory, CustomModelCacheKeyFactory>();

        base.OnConfiguring(optionsBuilder);
    }
}

// CustomModelCacheKeyFactory
public class CustomModelCacheKeyFactory : IModelCacheKeyFactory
{
    public object Create(DbContext context, bool designTime)
    {
        var contextWithSchema = context as AppDbContext;
        // O TenantId ou schema específico pode ser utilizado como chave, junto com o designTime.
        return (contextWithSchema?.CurrentSchema, context.GetType(), designTime);
    }
}

enter image description here

enter image description here

{
  "documentNumber": "03350593097",
  "workspaceName": "My_Work_1", 
  "phoneNumber": "27996356704", 
  "email": "[email protected]",
  "password": "123456",
  "confirmPassword": "123456",  
  "subscriptionPlanId": "0335a5fe-4754-4ed0-1434-08dd1937d5aa",//after starting the API get the ID from the table
  "cultureCode": "pt-BR",
  "isolated": true
}

该项目很大,我无法在这里发布整个源代码。您可以下载

entity-framework asp.net-core entity-framework-core
1个回答
0
投票

我创建了一个小型控制台应用程序,演示如何配置 EF Core 来生成不同架构的迁移,同时仍然利用默认架构的迁移生成。这些类是按逻辑组织的,它们的依赖关系将在稍后解释。

Program.cs
示例:组合所有内容

internal class Program
{
    static void Main(string[] args)
    {
        var connectionString = "Server=localhost,1419;Database=SOMultitenancy;User Id=sa;Password=Password12!;Encrypt=true;TrustServerCertificate=true";

        using var context1 = CreateTenantContext(connectionString, "tenant1");
        using var context2 = CreateTenantContext(connectionString, "tenant2");
        using var context3 = CreateTenantContext(connectionString, "tenant3");
        
        context1.Database.Migrate();
        context2.Database.Migrate();  
        context3.Database.Migrate();

        // At this point we have all tenants configured and all tables are created

        var o1 = context1.Orders.ToArray();
        var o2 = context2.Orders.ToArray();
        var o3 = context3.Orders.ToArray();
    }

    private static AppDbContext CreateTenantContext(string connectionString, string? tenantName)
    {
        var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
        optionsBuilder
            .UseSqlServerMultiTenant(connectionString, tenantName, AppDbContextModelConfiguration.BuildModel)
            .LogTo(s => Console.WriteLine(s));

        var context = new AppDbContext(optionsBuilder.Options);
        return context;
    }
}

定义测试上下文

public class AppDbContext : DbContext
{
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Order> Orders { get; set; }


    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }
}

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Order> Orders { get; set; }
}

public class Order
{
    public int Id { get; set; }
    public DateTime OrderDate { get; set; }
    public decimal Amount { get; set; }
    public int CustomerId { get; set; }
    public Customer Customer { get; set; }
}

定义
AppDbContextFactory
,这有助于为控制台应用程序生成迁移

public class AppDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
{
    /// <summary>
    /// Creates a new instance of AppDbContext for design-time tools like migrations.
    /// </summary>
    /// <param name="args">Command-line arguments.</param>
    /// <returns>An instance of AppDbContext.</returns>
    public AppDbContext CreateDbContext(string[] args)
    {
        // Define the connection string
        var connectionString = "Server=localhost,1419;Database=SOMultitenancy;User Id=sa;Password=Password12!;Encrypt=true;TrustServerCertificate=true";

        // Configure DbContextOptions
        var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
        optionsBuilder
            .UseSqlServerMultiTenant(connectionString, null, AppDbContextModelConfiguration.BuildModel);

        return new AppDbContext(optionsBuilder.Options);
    }
}

型号配置

您应该将模型配置移至此类中

public static class AppDbContextModelConfiguration
{
    public static readonly ConcurrentDictionary<string, IModel> ModelCache = new();

    public static ModelBuilder CreateModelBuilder()
    {
        // Create the ModelBuilder with the SQL Server conventions
        var modelBuilder = new ModelBuilder(ModelBuildingHelper.GetSqlServerConventionSet());

        return modelBuilder;
    }

    public static IModel BuildModel(string? schemaName = default)
    {
        schemaName ??= string.Empty;

        if (ModelCache.TryGetValue(schemaName, out var model))
            return model;

        var modelBuilder = CreateModelBuilder();

        if (!string.IsNullOrEmpty(schemaName))
            modelBuilder.HasDefaultSchema(schemaName);

        ConfigureModel(modelBuilder);
        model = modelBuilder.FinalizeModel();

        ModelCache.TryAdd(schemaName, model);

        return model;
    }

    private static void ConfigureModel(ModelBuilder modelBuilder)
    {
        // Configure Customer entity
        modelBuilder.Entity<Customer>(entity =>
        {
            entity.HasKey(e => e.Id);
            entity.Property(e => e.Name).IsRequired().HasMaxLength(100);
            entity.HasMany(e => e.Orders)
                .WithOne(o => o.Customer)
                .HasForeignKey(o => o.CustomerId);
        });

        // Configure Order entity
        modelBuilder.Entity<Order>(entity =>
        {
            entity.HasKey(e => e.Id);
            entity.Property(e => e.OrderDate).IsRequired();
            entity.Property(e => e.Amount).HasColumnType("decimal(18,2)");
        });
    }
}

SQL Server 约定助手

public static class ModelBuildingHelper
{
    private static readonly object _lock = new object();
    private static ConventionSet? _sqlServerConventionSet = null;

    public static ConventionSet GetSqlServerConventionSet()
    {
        if (_sqlServerConventionSet == null)
        {
            lock (_lock)
            {
                if (_sqlServerConventionSet == null)
                {
                    var serviceProvider = new ServiceCollection()
                        .AddSqlServer<DbContext>("no-connection")
                        .BuildServiceProvider();
                    var context = serviceProvider
                        .GetRequiredService<DbContext>();
                    var conventionSetBuilder = context.GetInfrastructure()
                        .GetRequiredService<IProviderConventionSetBuilder>();
                    _sqlServerConventionSet = conventionSetBuilder.CreateConventionSet();
                }
            }
        }

        return _sqlServerConventionSet;
    }
}

多租户配置扩展

public static class MultiTenancyDbContextOptionsExtensions
{
    public static DbContextOptionsBuilder UseSqlServerMultiTenant(this DbContextOptionsBuilder optionsBuilder, string connectionString, string? schemaName, Func<string?, IModel> buildModel)
    {
        optionsBuilder
            .UseSqlServer(connectionString, o => o.MigrationsHistoryTable("__EFMigrationsHistory", !string.IsNullOrEmpty(schemaName) ? schemaName : null))
            .UseModel(buildModel(schemaName))
            .ReplaceService<IMigrationsSqlGenerator, MultitenancySqlServerMigrationsSqlGenerator>();

        // Ignore pending model changes warning
        if (!string.IsNullOrEmpty(schemaName))
            optionsBuilder.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning));

        return optionsBuilder;
    }
}

覆盖特定于架构的操作的迁移

public class MultitenancySqlServerMigrationsSqlGenerator(
    MigrationsSqlGeneratorDependencies dependencies,
    ICommandBatchPreparer commandBatchPreparer)
    : SqlServerMigrationsSqlGenerator(dependencies, commandBatchPreparer)
{

    public override IReadOnlyList<MigrationCommand> Generate(
        IReadOnlyList<MigrationOperation> operations,
        IModel? model = null,
        MigrationsSqlGenerationOptions options = MigrationsSqlGenerationOptions.Default)
    {
        return base.Generate(RewriteOperations(operations, model, options), model, options);
    }

    private IReadOnlyList<MigrationOperation> RewriteOperations(
        IReadOnlyList<MigrationOperation> migrationOperations,
        IModel? model,
        MigrationsSqlGenerationOptions options)
    {
        string? defaultSchema = null;

        bool IsDefaultSchema(string? schemaName)
        {
            return schemaName == null || schemaName == defaultSchema;
        }

        var schema = Dependencies.CurrentContext.Context.Model.GetDefaultSchema();

        if (schema == null || schema == defaultSchema)
            return migrationOperations;

        foreach (var operation in migrationOperations)
        {
            switch (operation)
            {
                case CreateTableOperation createTableOperation:
                    if (IsDefaultSchema(createTableOperation.Schema))
                    {
                        createTableOperation.Schema = schema;

                        foreach (var ck in createTableOperation.CheckConstraints.Where(ck => IsDefaultSchema(ck.Schema)))
                        {
                            ck.Schema = schema;
                        }

                        foreach (var uk in createTableOperation.UniqueConstraints.Where(uk => IsDefaultSchema(uk.Schema)))
                        {
                            uk.Schema = schema;
                        }

                        foreach (var fk in createTableOperation.ForeignKeys)
                        {
                            if (IsDefaultSchema(fk.Schema))
                            {
                                fk.Schema = schema;
                                fk.PrincipalSchema = schema;
                            }
                        }
                    }
                    break;
                case TableOperation tableOperation:
                    if (IsDefaultSchema(tableOperation.Schema)) 
                        tableOperation.Schema = schema;
                    break;
                case DropTableOperation dropTableOperation:
                    if (IsDefaultSchema(dropTableOperation.Schema)) 
                        dropTableOperation.Schema = schema;
                    break;
                case RenameTableOperation renameTableOperation:
                    if (IsDefaultSchema(renameTableOperation.Schema)) 
                        renameTableOperation.Schema = schema;
                    break;
                case ColumnOperation columnOperation:
                    if (IsDefaultSchema(columnOperation.Schema)) 
                        columnOperation.Schema = schema;
                    break;
                case CreateIndexOperation createIndexOperation:
                    if (IsDefaultSchema(createIndexOperation.Schema)) 
                        createIndexOperation.Schema = schema;
                    break;
                case DropIndexOperation dropIndexOperation:
                    if (IsDefaultSchema(dropIndexOperation.Schema))
                        dropIndexOperation.Schema = schema;
                    break;
                case EnsureSchemaOperation ensureSchemaOperation:
                    if (IsDefaultSchema(ensureSchemaOperation.Name)) 
                        ensureSchemaOperation.Name = schema;
                    break;
                default:
                    //TODO: Add more operations
                    break;
            }
        }

        return migrationOperations;
    }
}

最后的笔记

正确设置所有内容后,您可以为默认架构生成迁移,并动态地为每个租户的架构重用它们。这种方法确保每个租户都有自己的迁移历史表和隔离的架构配置。

© www.soinside.com 2019 - 2024. All rights reserved.