我正在开发一个多租户 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);
}
}
{
"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
}
该项目很大,我无法在这里发布整个源代码。您可以下载。
我创建了一个小型控制台应用程序,演示如何配置 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)");
});
}
}
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;
}
}
正确设置所有内容后,您可以为默认架构生成迁移,并动态地为每个租户的架构重用它们。这种方法确保每个租户都有自己的迁移历史表和隔离的架构配置。