如何在 EF Core Code First 中自定义迁移生成?

Posted

技术标签:

【中文标题】如何在 EF Core Code First 中自定义迁移生成?【英文标题】:How to customize migration generation in EF Core Code First? 【发布时间】:2020-12-13 22:13:01 【问题描述】:

我的DbContext 中有一个特殊的基表类型。当从它继承时,我需要生成一个额外的“SQL”迁移操作来为它创建一个特定的触发器。它通过检查重叠范围来确保表结构是一致的。由于 SQL Server 中没有重叠的索引或检查约束,我必须使用触发器(在检查约束中使用函数会导致与迁移相同的问题以及 SQL 中的杂乱函数“命名空间”)。

由于在OnModelCreating 期间我没有找到任何创建触发器的方法,我想改变生成的迁移。但是该怎么做呢?

尝试使用SqlServerMigrationsSqlGeneratorSqlServerMigrationsAnnotationProvider,但顾名思义,它们仅用于最后阶段,即在生成 SQL 命令期间。这使得它们在使用迁移时有点“隐藏”。难以在需要时进行定制和事后维护。

考虑过使用CSharpMigrationOperationGenerator,这似乎非常适合我的需求。但是有一个问题——我不能访问这个类。也不是命名空间。

根据来源,该类位于Microsoft.EntityFrameworkCore.Migrations.Design 命名空间中并且是公共的。为了访问它,必须安装 Microsoft.EntityFrameworkCore.Design 包。

但它不起作用。

我在这里缺少什么?如何访问和继承这个类?或者也许有更好更合适的方法在迁移特定表的过程中自动创建触发器?

【问题讨论】:

【参考方案1】:

如何提供自己的ICSharpMigrationOperationGenerator 实现

考虑过使用 CSharpMigrationOperationGenerator 似乎非常适合我的需求。但是有一个问题——我不能访问这个类。也不是命名空间。

根据来源,此类位于 Microsoft.EntityFrameworkCore.Migrations.Design 命名空间中并且是公共的。为了访问它,必须安装 Microsoft.EntityFrameworkCore.Design 包。

但它不起作用。

我在这里缺少什么? 如何访问和继承这个类?

假设您在设计时调用以下 CLI 命令来添加新迁移:

dotnet ef migrations add "SomeMigration"

这是一个完整的示例控制台程序,它将使用一个名为 MyCSharpMigrationOperationGenerator 的自定义 ICSharpMigrationOperationGenerator 实现,继承自 CSharpMigrationOperationGenerator

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Migrations.Design;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace IssueConsoleTemplate

    public class MyCSharpMigrationOperationGenerator : CSharpMigrationOperationGenerator
    
        public MyCSharpMigrationOperationGenerator(CSharpMigrationOperationGeneratorDependencies dependencies)
            : base(dependencies)
        
        

        protected override void Generate(CreateTableOperation operation, IndentedStringBuilder builder)
        
            Console.WriteLine("\r\n\r\n---\r\nMyCSharpMigrationOperationGenerator was used\r\n---\r\n");
            base.Generate(operation, builder);
        
    
    
    public class MyDesignTimeServices : IDesignTimeServices
    
        public void ConfigureDesignTimeServices(IServiceCollection services)
            => services.AddSingleton<ICSharpMigrationOperationGenerator, MyCSharpMigrationOperationGenerator>();
    
    
    public class IceCream
    
        public int IceCreamId  get; set; 
        public string Name  get; set; 
    
    
    public class Context : DbContext
    
        public DbSet<IceCream> IceCreams  get; set; 

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        
            optionsBuilder
                .UseSqlServer(@"Data Source=.\MSSQL14;Integrated Security=SSPI;Initial Catalog=So63575132")
                .UseLoggerFactory(
                    LoggerFactory.Create(
                        b => b
                            .AddConsole()
                            .AddFilter(level => level >= LogLevel.Information)))
                .EnableSensitiveDataLogging()
                .EnableDetailedErrors();
        
    

    internal static class Program
    
        private static void Main()
        
        
    

MyCSharpMigrationOperationGenerator 类为每个添加的表输出以下行,以证明它被调用:

---
MyCSharpMigrationOperationGenerator was used
---

正如@KasbolatKumakhov 在他的评论中指出的那样,还应该指出从 2.2 引用 Microsoft.EntityFrameworkCore.Design has been changed 的方式。到 3.0:

从 EF Core 3.0 开始,它是一个 DevelopmentDependency 包。这意味着依赖项不会传递到其他项目,并且默认情况下您不能再引用它的程序集。 [...] 如果需要引用此包来覆盖 EF Core 的设计时行为,则可以在项目中更新 PackageReference 项元数据。

<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.0.0">
  <PrivateAssets>all</PrivateAssets>
  <!-- Remove IncludeAssets to allow compiling against the assembly -->
  <!--<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>-->
</PackageReference>

如何正确实现额外的MigrationOperation(例如用于触发器创建)

由于在 OnModelCreating 期间我没有找到任何创建触发器的方法,我想到了更改生成的迁移。但是该怎么做呢?

要正确执行此操作,您需要执行以下操作:

在相关表格中添加您自己的注释(例如MyPrefix:Trigger) 实现您自己的MigrationOperation(例如CreateTriggerMigrationOperation) 提供您自己的IMigrationsModelDiffer 实现(派生自MigrationsModelDiffer;这是内部的)返回您自己的MigrationOperation 提供您自己的ICSharpMigrationOperationGenerator 实现(派生自CSharpMigrationOperationGenerator),然后为您自己的MigrationOperation 生成C# 代码 提供您自己的IMigrationsSqlGenerator 实现(派生自SqlServerMigrationsSqlGenerator),然后将您自己的MigrationOperation 转换为SQL

【讨论】:

谢谢。如何注册 MyDesignTimeServices,以便调用 MyCSharpMigrationOperationGenerator? 你没有。它是由 EF Core 工具使用反射发现的。我发布的示例代码是完整的,应该按原样运行。它不会在任何地方注册MyDesignTimeServices。请参阅Design-time services 了解更多信息。这适用于所有命令行工具(在设计时)。如果您想在 runtime 应用迁移,则需要在 DI 容器中注册 ICSharpMigrationOperationGenerator(并且根本不需要 IDesignTimeServices 实现)。 知道了。谢谢。我似乎无法编辑您的帖子,所以请您添加此链接 (docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/…) 吗?这解释了如何访问 3.x 中提到的类 @KasbolatKumakhov 好点!我相应地更新了答案。 我一般不建议实施此解决方案。您将应用程序与设计时功能联系起来。具有命名空间 EntityFrameworkCore.Design 的程序集用于创建 csharp 迁移代码的 CLI ef 核心工具。程序集“Microsoft.EntityFrameworkCore.Design”将无法在托管环境中加载(例如,在 IIS 中托管时 - 启动错误 500.30)。我发布了一个替代解决方案来生成触发器。【参考方案2】:

这不完全符合您的要求,但它以低成本完成了类似的工作,并且对某人来说可能会派上用场。

using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Operations;

public static class MigrationBuilderExtensions

    public static void ConfigForOracle(this MigrationBuilder migrationBuilder)
    
        //For each table registered in the builder, let's create a sequence and a trigger
        foreach (CreateTableOperation createTableOperation in migrationBuilder.Operations.ToArray().OfType<CreateTableOperation>())
        
            string tableName = createTableOperation.Name;
            string primaryKey = createTableOperation.PrimaryKey.Columns[0];
            migrationBuilder.CreateSequence<int>(name: $"SQ_tableName", schema: createTableOperation.Schema);
            migrationBuilder.Sql($@"CREATE OR REPLACE TRIGGER ""TR_tableName""
                                    BEFORE INSERT ON ""tableName""
                                    FOR EACH ROW
                                    WHEN (new.""primaryKey"" IS NULL)
                                    BEGIN
                                        SELECT ""SQ_tableName"".NEXTVAL
                                        INTO   :new.""primaryKey""
                                        FROM   dual;
                                    END;");
        
    

您可以在扩展方法中做任何您想做的事情,然后在Migration.Up() 方法的末尾调用它。我使用它为 Oracle 11g 表创建序列和触发器以进行标识符增量。

【讨论】:

【参考方案3】:

打开您的迁移文件并更改您的 Up 方法。

然后使用包管理器控制台中的Update-Database 应用迁移。

类似这样的:

public partial class CreateDatabase : Migration

    protected override void Up(MigrationBuilder migrationBuilder)
    
        migrationBuilder.Sql("Some custom SQL statement");
        migrationBuilder.CreateTable(
            name: "Authors",
            columns: table => new
            
                AuthorId = table.Column<int>(nullable: false)
                    .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
                FirstName = table.Column<string>(nullable: true),
                LastName = table.Column<string>(nullable: true)
            ,
            constraints: table =>
            
                table.PrimaryKey("PK_Authors", x => x.AuthorId);
            );
    

【讨论】:

谢谢,但这不是我想要的。我需要一种自动的方法来做完全相同的事情,因为很难跟踪迁移中使用的每个表。只有在创建特定表时才需要添加 SQL 语句。不是每次。【参考方案4】:

我认为它不是为了修改 ef core csharp 代码生成。 但要生成自定义迁移语句(在我的情况下为触发器),我使用 SqlOperation 执行以下操作(缩短为相关)。

实现一个 ModelDiffer

public class MyMigrationsModelDiffer : MigrationsModelDiffer 

  public MyMigrationsModelDiffer(IRelationalTypeMappingSource typeMappingSource,
    IMigrationsAnnotationProvider migrationsAnnotations,
    IChangeDetector changeDetector,
    IUpdateAdapterFactory updateAdapterFactory,
    CommandBatchPreparerDependencies commandBatchPreparerDependencies)
    : base(typeMappingSource, migrationsAnnotations, changeDetector, updateAdapterFactory, commandBatchPreparerDependencies)  

  protected override IEnumerable<MigrationOperation> Diff(IModel source, IModel target, DiffContext diffContext) 
    return base.Diff(source, target, diffContext).Concat(GetTriggerTriggerDifferences(source, target));
  

  public override Boolean HasDifferences(IModel source, IModel target) 
    return base.HasDifferences(source, target) || HasTriggerAnnotationDifferences(source, target);
  

  public IEnumerable<MigrationOperation> GetTriggerTriggerDifferences(IModel source, IModel target) 
    if (source == null || target == null) 
      return new new List<MigrationOperation>(0);
    

    Dictionary<String, IAnnotation> triggerAnnotationPerEntity = new Dictionary<String, IAnnotation>();
    foreach (var entityType in source.GetEntityTypes()) 
      triggerAnnotationPerEntity[entityType.Name] = GetTableAnnotation(entityType);
    
    var operations = new List<MigrationOperation>();
    foreach (var entityType in target.GetEntityTypes()) 
      triggerAnnotationPerEntity.TryGetValue(entityType.Name, out IAnnotation sourceTriggerTable);
      IAnnotation targetTriggerTable = GetTableAnnotation(entityType);

      if (targetTriggerTable?.Value == sourceTriggerTable?.Value) 
        continue;
      

      Boolean isCreate = targetTriggerTable != null;
      String tableName = (entityType as EntityType)?.GetTableName();
      String primaryKey = entityType.FindPrimaryKey().Properties[0].Name;
      if (isCreate) 
        SqlOperation sqlOperation = new SqlOperation();
        sqlOperation.Sql = $@"CREATE TRIGGER...";
        operations.Add(sqlOperation);
      
      else 
        // drop trigger sqloperation
      
    
    return operations;
  

  private static IAnnotation GetTableAnnotation(IEntityType entityType) 
    return entityType.GetAnnotations()?.FirstOrDefault(x => x.Name == "WantTrigger");
  

  public Boolean HasTriggerAnnotationDifferences(IModel source, IModel target) 
    return GetTriggerTriggerDifferences(source, target).Any();
  

替换 DbContext 中不同的模型

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 
  base.OnConfiguring(optionsBuilder);
  if (optionsBuilder == null) 
    return;
  
  optionsBuilder.ReplaceService<IMigrationsModelDiffer, MyMigrationsModelDiffer>();

用注释标记所需的模型。

builder.Entity<MyTable>().HasAnnotation("WantTrigger", "1.0");

【讨论】:

谢谢,但我的目标是为用户提供完整的迁移步骤,其中所有操作在生成 SQL 脚本之前都是可见的。因此可以事先进行任何更改并进行测试。

以上是关于如何在 EF Core Code First 中自定义迁移生成?的主要内容,如果未能解决你的问题,请参考以下文章

EF Core中的DB First与Code First

如何使用EF Core Code-First桥接自己的表

EF Core开发模式之Code First

EF Core 2.0使用MsSql/Mysql实现DB First和Code First

持续部署的 EF Core Code First 运行时迁移回滚策略

用于 ASP.NET Core 应用程序的 EF Code First 迁移替代方案