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

问题描述

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

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

尝试使用SqlServerMigrationsSqlGeneratorSqlServerMigrationsAnnotationProvider,但顾名思义,它们仅在SQL命令生成期间的最后阶段使用。使用迁移时,这使它们有点看不见。难以在需要时进行自定义,然后再进行维护。

考虑使用CSharpMigrationOperationGenerator似乎很适合我的需求。但是有一个问题-我无法访问此类。也不是名称空间。

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

但这不起作用。

我在这里想念什么?如何访问和继承此类?也许有更好更好的方法可以在特定表的迁移过程中自动创建触发器?

解决方法

如何提供自己的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
                .UseMySQL(@"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
,

这不完全是您所要求的,但它以低成本完成了类似的工作,并且可能对某人有用。

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

,

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

我认为不打算修改ef核心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");

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...