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

8
我的DbContext 中有一种特殊的基本表类型。当从它继承时,我需要生成一个额外的“SQL”迁移操作来创建一个特定的触发器。它通过检查重叠范围来确保表结构的一致性。由于 SQL Server中没有重叠的索引或检查约束,因此必须使用触发器(在检查约束中使用函数会导致相同的迁移问题,并且使 SQL 函数“命名空间”混乱)。
我还没有找到在 OnModelCreating期间创建触发器的方法。但是如何做呢?
尝试使用 SqlServerMigrationsSqlGeneratorSqlServerMigrationsAnnotationProvider,但是正如它们的名称所示,它们仅用于最后阶段,在生成 SQL 命令期间使用。这使得当使用迁移时它们有点“隐藏”。难以在需要自定义和维护时进行修改。
考虑使用 CSharpMigrationOperationGenerator,它似乎非常适合我的需求。但是有一个问题-我无法访问此类。也无法访问其名称空间。
据源代码显示,该类驻留在 Microsoft.EntityFrameworkCore.Migrations.Design 名称空间中,是公共的。为了访问它,必须安装一个 Microsoft.EntityFrameworkCore.Design 包。
但是它不起作用。
我漏掉了什么?如何访问和继承此类?或者也许有一个更好,更合适的方法来自动创建特定表的迁移触发器?
4个回答

14

如何提供自己的ICSharpMigrationOperationGenerator实现

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

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

但是它不起作用。

我错过了什么?如何访问并继承此类?

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

dotnet ef migrations add "SomeMigration"

这是一个完全可用的示例控制台程序,它将使用自定义的ICSharpMigrationOperationGenerator实现,名为MyCSharpMigrationOperationGenerator,继承自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到3.0引用Microsoft.EntityFrameworkCore.Design的方式已经改变

Starting with EF Core 3.0, it is a DevelopmentDependency package. This means that the dependency won't flow transitively into other projects, and that you can no longer, by default, reference its assembly. [...] If you need to reference this package to override EF Core's design-time behavior, then you can update PackageReference item metadata in your project.

<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的过程

1
你不需要注册MyDesignTimeServices,EF Core工具会使用反射自动发现它。我发布的示例代码已经完整,可以直接运行。它不需要在任何地方注册MyDesignTimeServices。有关更多信息,请参见设计时服务。这适用于所有命令行工具(在_设计时_)。如果您想在_运行时_应用迁移,则需要在DI容器中注册ICSharpMigrationOperationGenerator(并且根本不需要IDesignTimeServices实现)。 - lauxjpn
1
@DiegoFrehner 嗯,这真的取决于您的场景。 我发布代码的实现不应该与IIS一起使用。它使用了 ConfigureDesignTimeServices,旨在通过 dotnet ef 从控制台运行。 它还明确解决了OP想要解决问题的方式。 话虽如此,我确实概述了另一种迁移操作的正确实现方式,在 如何正确实现其他MigrationOperation(例如触发器创建) 中。 如果有人需要实际的代码,我很乐意提供一些。 - lauxjpn
1
另外,作为一般性的评论,在实际生产应用中,永远不要通过你的应用程序来应用迁移。这是我和 EF Core 团队强烈反对的。总是从你的迁移生成脚本,验证它们,在测试数据库上运行它们,然后手动将它们应用到生产数据库(在首先备份之后)。 - lauxjpn
对我来说不起作用,我认为稍微解释一下正在发生的事情会很好。生成方法从未被调用过,可能与DesignTimeService有关。 - Bamdad
@Bamdad 代码应该无误。您没有描述您采取了哪些步骤。您是否按照我发布的代码完全运行了它?您是否运行了 dotnet ef migrations add "SomeMigration" 命令?运行命令时是否有任何错误消息?关于描述正在发生的事情:这都是非常高级的东西。给出一份充分的解释并说明其工作原理需要太多时间。如果您有具体问题,请随时提问。否则,请查看 EF Core 源代码或文档(例如关于 IDesignTimeServices)。 - lauxjpn
显示剩余9条评论

6

虽然不完全符合你的要求,但它可以以低成本完成类似的工作,并且对某些人可能非常有用。

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 表创建序列和触发器以进行标识符递增。


5

我认为不打算修改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脚本之前可以看到所有操作。因此,任何更改都可以事先进行测试。 - Kasbolat Kumakhov

2

打开您的迁移文件并更改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);
            });
    }
}

3
谢谢,但那不是我想要的。我需要一种自动化的方式来完成完全相同的事情,因为跟踪每个在迁移中使用的表格将非常困难。只有在创建特定的表格时才需要添加 SQL 语句,而不是每次都要添加。 - Kasbolat Kumakhov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接