EF Core – Custom Migrations (高级篇)

前言

会写这篇是因为最近开始大量使用 SQL Server Trigger 来维护冗余 (也不清楚这路对不对).

EF Core migrations 没有支持 Trigger Github Issue, 能找到相关的 Laraue.EfCoreTriggers, 但 star 太少, 不敢用.

于是计划自己实现一个简单版本符合自己用就好.

更新 09-11-2022: EF Core 7.0 breaking changes 提醒:

EF7 以后, 所有使用 Trigger 的 Table 都需要 Config 声明哦.

 

主要参考:

Add support for managing Triggers with EF migration

Laraue.EfCoreTriggers

Custom Migrations Operations

How to customize migration generation in EF Core Code First?

Design-time DbContext Creation 

EF Core Add Migration Debugging  

MigrationsModelDiffer.cs

 

How EF Core Migrations Work?

要搞底层东西, 首先要摸清楚它怎么 work 的. 

首先是 build model, 数据库表结构

然后运行 migrations command

dotnet ef migrations add init

Ef Core Design 会创建出 migrations file (我们熟悉的 Up, Down)

如果想做一些调整, 可以直接修改这个 file. 比如 migrationBuilder.Sql()

然后运行 update database command

dotnet ef database update

Ef Core Design 会依据不同的 SQL Provider 生产出对应的 SQL Command 去 update database.

 

The Official Way

在了解 migrations 的流程后, 下一步就是要知道如何扩展它.

Custom Migrations Operations

这一篇就教了如果去写自己的 Operations 来扩展 Migrations.

首先创建一个 migrationBuilder 扩展方法, 里头调用 migrationBuilder.Sql("SQL command here...");

然后在 migration file (就是那个 Up Down 的 class) 里调用

呃...这不就是直接修改 migrations file, 写上 SQL Command 吗... (也算扩展 ?)

文章里还说到, 要支持多个 SQL Provider 所以必须写多种 SQL Command.

除了上面这种直接的方法, 文章也给出另一种没有那么直接的方式

首先做一个 MigrationOperation

然后不直接调用 SQL Command, 只把 operation add 进去 builder

最后通过来扩展 SqlServerMigrationsSqlGenerator 来实现 operations to SQL command.

internal class MyMigrationsSqlGenerator : SqlServerMigrationsSqlGenerator
{
    public MyMigrationsSqlGenerator(
        MigrationsSqlGeneratorDependencies dependencies,
        IRelationalAnnotationProvider migrationsAnnotations)
        : base(dependencies, migrationsAnnotations)
    {
    }

    protected override void Generate(
        MigrationOperation operation,
        IModel model,
        MigrationCommandListBuilder builder)
    {
        if (operation is CreateUserOperation createUserOperation)
        {
            Generate(createUserOperation, builder);
        }
        else
        {
            base.Generate(operation, model, builder);
        }
    }

    private void Generate(
        CreateUserOperation operation,
        MigrationCommandListBuilder builder)
    {
        var sqlHelper = Dependencies.SqlGenerationHelper;
        var stringMapping = Dependencies.TypeMappingSource.FindMapping(typeof(string));

        builder
            .Append("CREATE USER ")
            .Append(sqlHelper.DelimitIdentifier(operation.Name))
            .Append(" WITH PASSWORD = ")
            .Append(stringMapping.GenerateSqlLiteral(operation.Password))
            .AppendLine(sqlHelper.StatementTerminator)
            .EndCommand();
    }
}
View Code

记得要把原本的 SqlServerMigrationsSqlGenerator 替换掉哦

 

 

实现思路

Official way 并不能解决我们的问题, 我们需要从 modelBuilder 阶段开始去写 Trigger. 然后 generate 出正确的 migration file, 而不是直接修改 migration file.

至于 migration file 里头是直接写 SQL Command 或者使用 operation 在交由 SqlServerMigrationsSqlGenerator 去实现 SQL command, 这区别不大.

在参考了 Laraue.EfCoreTriggers 源码后, 发现它的扩展方式是 IMigrationsModelDiffer.

IMigrationsModelDiffer 是 modelBuilder to migration file 过程中会用到的一个功能. 它会判断之前和之后的区别, 来生产 migration file.

通过扩展它就可以分析 modelBuilder 的结构, 然后生产 migration file.

modelBuilder 有一个扩展的方式是 AddAnnotation, 可以任意加入 key-value

然后在 IMigrationsModelDiffer 里头通过识别加入的 Annotation, 就可以修改 migration file, migration file 能扩展的地方是 .Sql()

以上就是 Laraue.EfCoreTriggers 的扩展方式了.

还有一篇 How to customize migration generation in EF Core Code First? 也提到了如何扩展 EF Core Migrations.

答题人正是 MySQL provider for EF Core 的 Lead developer.

 

5 个步骤, 

1. 添加自己的 annotation. (上面讲过了. 没问题)

2. 自定义 MIgrationOperation (Official way 讲过了, 没问题)

3. IMigrationsModelDiffer (和 Laraue.EfCoreTriggers 一样, 没问题, 提醒: 这个是 internal class 哦, EF Core 并没有 public 让我们扩展的意思, 但也没有其它的 way 了)

4. ICSharpMigrationOperationGenerator 

这个是新东西, 它就是负责把 modelBuilder 做成 migration file 的幕后黑手. 负责 generate C# code, 所以扩展它的话, 几乎可以完全控制 migration file 里的所有代码了.

5. SqlServerMigrationsSqlGenerator (Official way 讲过了, 没问题)

 

小总结

到这里我们搞清楚了几个重要的东西.

modelBuilder 负责描述数据库结构, 它可以通过 AddAnnotation key-value 来添加自定义的表述信息. (所以它负责表达而已)

ICSharpMigrationOperationGenerator 负责把 modelBuilder 解析, 然后生产 C# migration file. 间中还会用到 IMigrationsModelDiffer 来对比之前的 model 和之后的 model 哪里不同了.

migration file 里的 C# code 主要就是做一堆的 operation, 我们也可以自定义自己的 C# code 去做 operation (Official way)

最后 migration file 做出的 operations 被 SqlServerMigrationsSqlGenerator (或者其它 Provider 的 generator) 解析编译成最终的 SQL Command. 

 

 

逐个测试

我们先过一圈, 感受一下, 最后才决定如何实现 Trigger.

Custom Annotation

modelBuilder.Entity<Color>().HasAnnotation("Trigger", "SQL Command");

IMigrationsModelDiffer

#pragma warning disable EF1001 // Internal EF Core API usage.
public class MyMigrationsModelDiffer : Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer
{
    public MyMigrationsModelDiffer(
      IRelationalTypeMappingSource typeMappingSource,
      IMigrationsAnnotationProvider migrationsAnnotationProvider,
      IRowIdentityMapFactory rowIdentityMapFactory,
      CommandBatchPreparerDependencies commandBatchPreparerDependencies) : base(typeMappingSource, migrationsAnnotationProvider, rowIdentityMapFactory, commandBatchPreparerDependencies)
    { }

    public override IReadOnlyList<MigrationOperation> GetDifferences(IRelationalModel? source, IRelationalModel? target)
    {
        var x = source?.GetAnnotations();
        var y = target?.GetAnnotations();
        return base.GetDifferences(source, target);
    }
}
#pragma warning restore EF1001 // Internal EF Core API usage.

 

还要 ReplaceService 哦

services.AddDbContext<ApplicationDbContext>(options =>
{
    options.UseSqlServer("Server=192.168.1.152;Database=TestEFCore;Trusted_Connection=True;MultipleActiveResultSets=true")
    .ReplaceService<IMigrationsModelDiffer, MyMigrationsModelDiffer>();
});

ICSharpMigrationOperationGenerator

public class MyCSharpMigrationOperationGenerator : CSharpMigrationOperationGenerator
{
    public MyCSharpMigrationOperationGenerator(CSharpMigrationOperationGeneratorDependencies dependencies) : base(dependencies)
    {
        Console.Write("hello world");
    }
    protected override void Generate(CreateTableOperation operation, IndentedStringBuilder builder)
    {
        base.Generate(operation, builder);
        var www = builder.ToString();
    }
}

这个 C# generator 是在 Design Time 时做的. 它不是用 ReplaceService 而是通过依赖注入去 override 的.

public class MyDesignTimeServices : IDesignTimeServices
{
    public void ConfigureDesignTimeServices(IServiceCollection services)
        => services.AddSingleton<ICSharpMigrationOperationGenerator, MyCSharpMigrationOperationGenerator>();
}

顺便说一下,  如果是做测试 Console App 的话. Design Time 要另外写 Factory, 参考: Design-time DbContext Creation

public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
{
    public ApplicationDbContext CreateDbContext(string[] args)
    {
        Debugger.Launch();
        var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
        optionsBuilder
            .UseSqlServer("Server=192.168.1.152;Database=TestEFCore;Trusted_Connection=True;MultipleActiveResultSets=true")
            .ReplaceService<IMigrationsModelDiffer, MyMigrationsModelDiffer>();
        return new ApplicationDbContext(optionsBuilder.Options);
    }
}

注: Debugger.Launch(); 是为了调试用的. 参考: EF Core Add Migration Debugging,

这特调试不是一般的 F5 启动那种, 而是要调试 ModelDiffer 这种 design time 的代码, 通常是 cmd dotnet ef migrations add WhateverName 启动的.

ISqlServerMigrationsSqlGenerator

public class MyMigrationsSqlGenerator : SqlServerMigrationsSqlGenerator
{
    public MyMigrationsSqlGenerator(
        MigrationsSqlGeneratorDependencies dependencies,
        IRelationalAnnotationProvider migrationsAnnotations)
        : base(dependencies, migrationsAnnotations)
    {
    }

    protected override void Generate(MigrationOperation operation, IModel? model, MigrationCommandListBuilder builder)
    {
        base.Generate(operation, model, builder);
    }
}

这个也需要 ReplaceService

services.AddDbContext<ApplicationDbContext>(options =>
{
    options.UseSqlServer("Server=192.168.1.152;Database=TestEFCore;Trusted_Connection=True;MultipleActiveResultSets=true")
    .ReplaceService<IMigrationsSqlGenerator, MyMigrationsSqlGenerator>()
    .ReplaceService<IMigrationsModelDiffer, MyMigrationsModelDiffer>();
});

注: 所有 ReplaceService 只能一次哦, 之前在 Library use EF 的时候有讲过, 如果是封装 Library 的话要注意了.

 

我怎么做?

回到我最初的目的, 想让 migration 来维护 "我的 Trigger". 就目前看,一个非常完整的方案应该是 

定义好 modelBuilder 的扩展. 自定义 C# generator 编辑并调用自定义的 opration 函数, 然后由不同的 SQL Provider 去解析 operation 生成 SQL command.

所以需要 Custom Annotation, IMigrationsModelDiffer, ICSharpMigrationOperationGenerator, ISqlServerMigrationsSqlGenerator, 全部用上.

很显然我并不会这样折腾自己...所以最简单的方式就是像 Laraue.EfCoreTriggers 那样, 只要 add custom annotation, 然后扩展 IMigrationsModelDiffer 里头调用 build-in 的 SQL operation 函数 

也就是 .Sql() 啦, 这样就够我自己用了. 主要参考: MigrationsModelDiffer.cs

 

实战

关键就在 IMigrationsModelDiffer 如何解析自定义的 annotation.

source 是 previous, target 是 current. 通过各做对比就可以创建出不同的 SqlOperation, 比如 CREATE TRIGGER, DROP TRIGGER 等等.

#pragma warning disable EF1001 // Internal EF Core API usage.
public class MyMigrationsModelDiffer : Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer
{
    public MyMigrationsModelDiffer(
      IRelationalTypeMappingSource typeMappingSource,
      IMigrationsAnnotationProvider migrationsAnnotationProvider,
      IRowIdentityMapFactory rowIdentityMapFactory,
      CommandBatchPreparerDependencies commandBatchPreparerDependencies) : base(typeMappingSource, migrationsAnnotationProvider, rowIdentityMapFactory, commandBatchPreparerDependencies)
    { }

    public override IReadOnlyList<MigrationOperation> GetDifferences(IRelationalModel? source, IRelationalModel? target)
    {
        var sourceModel = source?.Model;
        var targetModel = target?.Model;
        var oldEntityTypeNames = sourceModel?.GetEntityTypes().Select(x => x.Name) ?? Enumerable.Empty<string>();
        var newEntityTypeNames = targetModel?.GetEntityTypes().Select(x => x.Name) ?? Enumerable.Empty<string>();
        var commonEntityTypeNames = oldEntityTypeNames.Intersect(newEntityTypeNames);
        if (targetModel != null)
        {
            // modelBuilder.Entity<Product>().Metadata.Model.AddAnnotation("n1", "n1");
            var annotations = targetModel.GetAnnotations().Select(a => a.Name);

            // modelBuilder.Entity<Product>().HasAnnotation("n2", "n2");
            var e = targetModel.GetEntityTypes().Single(e => e.Name == "TestEFCore.Product").GetAnnotations().Select(e => e.Name).ToList();

            // modelBuilder.Entity<Product>().Property(e => e.Name).HasAnnotation("n3", "n3");
            var p = targetModel.GetEntityTypes().Single(e => e.Name == "TestEFCore.Product").GetProperty(nameof(Product.Name)).GetAnnotations().Select(e => e.Name).ToList();

            // modelBuilder.Entity<Product>().HasMany(e => e.Colors).WithOne().HasAnnotation("n6", "n6").HasForeignKey(e => e.ProductId).HasAnnotation("n5", "n5")
            // .OnDelete(DeleteBehavior.Cascade).HasAnnotation("n4", "n4");
            var f = targetModel.GetEntityTypes().Single(e => e.Name == "TestEFCore.Product").GetReferencingForeignKeys().Select(k => k.GetAnnotations().Select(e => e.Name)).ToList(); // n4, n5, n6
        }

        IReadOnlyList<MigrationOperation> migrationOperations = base.GetDifferences(source, target);
        var finalMigrationOperations = migrationOperations.Concat(new List<MigrationOperation>
        {
            new SqlOperation
            {
                // 要支持 multiple provider 的话参考: Laraue.EfCoreTriggers, 它是在 AddAnnotation 阶段就已经 build 好 SQL command 了.
                Sql = "SQL command here ..."
            }
        }).ToList();
        return finalMigrationOperations;
    }
}
#pragma warning restore EF1001 // Internal EF Core API usage.

好了, 关键都有了,剩下的就各自发挥吧. 我就不写下去了.

 

目前遇到的局限

想在 IMigrationsModelDiffer | ISqlServerMigrationsSqlGenerator 注入 Service 是做不到的. 

因为 EF Core 有 internal 的 provider for 这 2 个 service, 外部是扩展不了的. 或者至少目前是没有 right way 去做到的. 

EF cannot resolve custom IMigrationsSqlGenerator

 

Entity Model 常用属性与方法

这里补上一些常用到的属性和方法。

Entity

public class Category
{
  public int Id { get; set; }
  public string Name { get; set; } = "";
  public List<Product> Products { get; set; } = [];
}

public class Product
{
  public int Id { get; set; }
  public string Name { get; set; } = "";
  public int CategoryId { get; set; }
  public Category Category { get; set; } = null!;
}

DbContext

public class ApplicationDbContext : DbContext
{
  public DbSet<Product> Products => Set<Product>();
  public DbSet<Category> Categories => Set<Category>();

  protected override void OnModelCreating(ModelBuilder modelBuilder)
  {
    modelBuilder.Entity<Category>().ToTable("Category");
    modelBuilder.Entity<Category>().Property(e => e.Name).HasMaxLength(256);
    modelBuilder.Entity<Category>()
      .HasMany(e => e.Products)
      .WithOne(e => e.Category)
      .HasForeignKey(e => e.CategoryId)
      .OnDelete(DeleteBehavior.Cascade);

    modelBuilder.Entity<Product>().ToTable("Product");
    modelBuilder.Entity<Product>().Property(e => e.Name).HasMaxLength(256);
  }

  protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  {
    optionsBuilder
      .UseSqlServer("Server=192.168.0.152;Database=SimpleTestEFCore;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True")
      .LogTo(Console.WriteLine);
  }
}

两个 entity,1-n (一对多) 关系。

EntityType 是一个核心的 class,很多属性方法都从它展开

FindEntityType

await using var db = new ApplicationDbContext();
var categoryEntityType = db.Model.FindEntityType(typeof(Category))!;
Console.WriteLine(categoryEntityType.ClrType == typeof(Category)); // true

透过 db.Model.FindEntityType 可以获取到 EntityType。(找不到会返回 null)

我们定义的 class Category 在这里被称为 ClrType。

GetEntityTypes

FindEntityType 是找一个,GetEntityTypes 则是把所有的 EntityType 调出来

var allEntityTypes = db.Model.GetEntityTypes();

var categoryEntityType = allEntityTypes.SingleOrDefault(e => e.ClrType == typeof(Category)); // 等价于 FindEntityType

GetTableName

顾名思义,就是拿 Entity 对应数据库的名字

var productEntityType = db.Model.FindEntityType(typeof(Product))!;
Console.WriteLine(productEntityType.GetTableName()); // Product

假如没有 ToTable

默认 table name 会是 plural

Console.WriteLine(db.Model.FindEntityType(typeof(Product))!.GetTableName());  // Products
Console.WriteLine(db.Model.FindEntityType(typeof(Category))!.GetTableName()); // Categories

ClrType

上面讲过了,ClrType 就是拿 Entity 的 class。

Console.WriteLine(
  db.Model.FindEntityType(typeof(Product))!.ClrType == typeof(Product) // true
);  

总之,要分清楚,我们定义的 class Product 是 EntityType.ClrType,ProductEntityType 指的是 class Product 加上各种 fluent api 定义的信息。

IsOwned

如果是 Entity 是 Owned Entity Types,那么 IsOwned 就是 true。

public class Order
{
  public int Id { get; set; }
  public OrderCustomerInfo CustomerInfo { get; set; } = null!;
  public decimal Amount { get; set; }
}

public class OrderCustomerInfo
{
  public string Name { get; set; } = "";
  public string Phone { get; set; } = "";
}

OrderCustomerInfo 是 Owned Entity Types。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  modelBuilder.Entity<Order>(
    builder =>
    {
      builder.ToTable("Order");
      builder.Property(e => e.Amount).HasPrecision(19, 2);
      builder.OwnsOne(e => e.CustomerInfo, builder =>
      {
        builder.Property(e => e.Name).HasMaxLength(256);
        builder.Property(e => e.Phone).HasMaxLength(256);
      });
    });
}

效果

Console.WriteLine(db.Model.FindEntityType(typeof(OrderCustomerInfo))!.IsOwned()); // true
Console.WriteLine(db.Model.FindEntityType(typeof(Order))!.IsOwned());             // false

注:Complex Types 不是 Owned Entity Types 哦。

GetProperty

EntityType.GetProperty 返回的是 Microsoft.EntityFrameworkCore.Metadata.IProperty 对象

var productEntityType = db.Model.FindEntityType(typeof(Product))!;

var nameProperty = productEntityType.GetProperty(nameof(Product.Name));

Console.WriteLine(nameProperty.ClrType == typeof(string)); // true

Console.WriteLine(nameProperty.GetColumnName());           // "Name"

Console.WriteLine(nameProperty.PropertyInfo == typeof(Product).GetProperty("Name")); // true

// DeclaringType 指向回 IProperty 所属的 EntityType 
Console.WriteLine(nameProperty.DeclaringType == productEntityType); // true

透过它可以拿到相关的信息,比如 Property 对应的数据库 Column Name。

这里点到为止,下一 part 我们才展开 IProperty,先继续 EntityType。

StoreObjectIdentifier for GetColumnName

补充说明一下 GetColumnName 方法

var productEntityType = db.Model.FindEntityType(typeof(Product))!;
var property = productEntityType.GetProperty("Name");

var productStoreObjectIdentifier = StoreObjectIdentifier.Create(productEntityType, StoreObjectType.Table)!.Value;
var columnName = property.GetColumnName(productStoreObjectIdentifier); // "Name"

最好是加上 StoreObjectIdentifier,这样拿会比较准。

EF Core 5.0 有一个 breaking changes -- IProperty.GetColumnName() is now obsolete

5.0 以后一定需要传入 StoreObjectIdentifier 才可以使用 GetColumnName。

但 7.0 后又改了回来,具体原因我不太清楚,总之用 StoreObjectIdentifier 会比较准,没用的话,当遇上 InheritanceSplitting, View 这些情况时,可能会拿错 column name。

GetAnnotation

Annotation 有点像是 Attributes,总之它里面包含了各种信息。看例子

var productEntityType = db.Model.FindEntityType(typeof(Product))!;

var tableName = productEntityType.GetAnnotation("Relational:TableName").Value; // Product

var productNameMaxLength = productEntityType.GetProperty("Name").GetAnnotation("MaxLength").Value; // 256

EntityType 和 IProperty 都可以有多个 Annotation,每一个 Annotation 负责保存一个 Name,一个 Value。

productEntityType.GetProperty("Name").GetAnnotations().Single(e => e.Name == "MaxLength").Value; // 256

GetAnnotations 可以获取所有的 Annotations。

AddAnnotation

await using var db = new ApplicationDbContext();
var productEntityType = db.Model.FindEntityType(typeof(Product))!;
productEntityType.AddRuntimeAnnotation("MyAnnotation", "Value");
Console.WriteLine(productEntityType.FindRuntimeAnnotation("MyAnnotation")?.Value);  // "Value"
Console.WriteLine(productEntityType.FindAnnotation("MyAnnotation")?.Value == null); // true

db.Model 拿到的 EntityType 是 build 好了的,基本上不能改动了。

勉强要加 Annotation,也只能加 RunTimeAnnotation。

而 ModelBuilder.Model 拿到的 EntityType 则是 IMutableEntityType

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  modelBuilder.Entity<Category>().ToTable("Category");

  IMutableEntityType categoryEntityType = modelBuilder.Model.FindEntityType(typeof(Category))!;
  categoryEntityType.AddAnnotation("MyAnnotation", "Value");
  Console.WriteLine(categoryEntityType.FindAnnotation("MyAnnotation")?.Value); // "Value"
}

顾名思义,就是还能修改的 EntityType。

我们可以透过 AddAnnotation 添加 Annotation。

在日常开发中,OnModelCreating 阶段我们会 read / write IMutableEntityType,动态的增加各种逻辑。

而到了 DbContext.Model 阶段,EntityType 已经定型了,我们只会做 read 操作。

FindPrimaryKey

每个 Entity 都会有 PrimaryKey 

var productEntityType = db.Model.FindEntityType(typeof(Product))!;
var primaryKey = productEntityType.FindPrimaryKey()!;
Console.WriteLine(primaryKey.Properties.Single().Name == "Id"); // true  

Properties 就是 IProperty 咯,PrimaryKey 允许有 multiple property,但通常是一个啦。

FindDeclaredPrimaryKey

当遇到继承 Entity 时,FindPrimaryKey 能找到祖先定义的 PrimaryKey,而 FindDeclaredPrimaryKey 只会查找当前 EntityType 的 PrimaryKey。

GetForeignKeys

获取 Foreign Key 相关信息

var productEntityType = db.Model.FindEntityType(typeof(Product))!;
var foreignKeys = productEntityType.GetForeignKeys(); // 拿 Product 的所有 Foreign Key
var foreignKey = foreignKeys.ElementAt(0); // 我们的例子只有一个 Foreign Key

// 这个 Foreign Key 是 property CategoryId;
Console.WriteLine(foreignKey.Properties.ElementAt(0).Name == "CategoryId"); // true

// PrincipalEntityType 指的是这个 Foreign Key 的 Principal 是谁,我们例子中是 Category (Category 和 Product 是 1-n 关系)
Console.WriteLine(foreignKey.PrincipalEntityType == db.Model.FindEntityType(typeof(Category))); // true;

// DeclaringEntityType 指的是这个 Foreign Key 的 Entity 是谁,我们的例子就是 Product 咯
Console.WriteLine(foreignKey.DeclaringEntityType == db.Model.FindEntityType(typeof(Product)));  // true;

GetReferencingForeignKeys

GetReferencingForeignKeys 也是拿 Foreign Key,只不过是从 PrincipalEntityType 出发去拿,起点不一样,但终点是一样的。

var categoryEntityType = db.Model.FindEntityType(typeof(Category))!;
var productEntityType = db.Model.FindEntityType(typeof(Product))!;

var foreignKeyFromProduct = productEntityType.GetForeignKeys().ElementAt(0);
var foreignKeyFromCategory = categoryEntityType.GetReferencingForeignKeys().ElementAt(0); // 从 Principal EntityType 出发

Console.WriteLine(foreignKeyFromProduct == foreignKeyFromCategory); // true 同一个 Foreign Key 来的

GetNavigations

Navigation 就是那些 ForeignKey 的链接对象

看例子

var productEntityType = db.Model.FindEntityType(typeof(Product))!;

var navigation = productEntityType.GetNavigations().ElementAt(0);
Console.WriteLine(navigation.Name); // "Category"
Console.WriteLine(navigation.ForeignKey.Properties.ElementAt(0).Name); // "CategoryId"
Console.WriteLine(navigation.ForeignKey.DependentToPrincipal == navigation); // true -- 通过 ForeignKey 也可以拿到相关的 Navigation 哦
Console.WriteLine(navigation.DeclaringEntityType.ClrType.Name); // "Product"
Console.WriteLine(navigation.TargetEntityType.ClrType.Name);    // "Category"

上面这个是 Dependent to Principal。

下面这个是反过来 Principal to Dependent。(Principal 指的是 Category,我们的例子是 Category -> Product 是 1-n 关系,Foreign Key 记入在 Product 身上,所以 Category 是 Principal)。

var categoryEntityType = db.Model.FindEntityType(typeof(Category))!;

var navigation = categoryEntityType.GetNavigations().ElementAt(0);
Console.WriteLine(navigation.Name); // "Products"
Console.WriteLine(navigation.ForeignKey.PrincipalToDependent == navigation); // true -- 这是 PrincipalToDependent,Property 是 "Products"

Console.WriteLine(
  // DependentToPrincipal (PropertyName 是 Category)
  navigation.ForeignKey.DependentToPrincipal == db.Model.FindEntityType(typeof(Product))!.GetNavigations().ElementAt(0) // true
);

GetSkipNavigations

GetNavigations 只能拿到 1-n 关系,n-n (多对多) 关系拿不到,需要靠 GetSkipNavigations。

我们把 Category 和 Product 换成 n-n 关系。

Entity

public class Category
{
  public int Id { get; set; }
  public string Name { get; set; } = "";
  public List<Product> Products { get; set; } = [];
}

public class Product
{
  public int Id { get; set; }
  public string Name { get; set; } = "";
  public List<Category> Categories { get; set; } = [];
}

DbContext

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  modelBuilder.Entity<Category>().ToTable("Category");
  modelBuilder.Entity<Category>().Property(e => e.Name).HasMaxLength(256);

  modelBuilder.Entity<Product>().ToTable("Product");
  modelBuilder.Entity<Product>().Property(e => e.Name).HasMaxLength(256);
}

尝试拿 navigation

var categoryEntityType = db.Model.FindEntityType(typeof(Category))!;
Console.WriteLine(categoryEntityType.GetNavigations().Count()); // 0

GetNavigations 拿不到,要用 GetSkipNavigations

var categoryEntityType = db.Model.FindEntityType(typeof(Category))!;
var productEntityType = db.Model.FindEntityType(typeof(Product))!;

var categorySkipNavigation = categoryEntityType.GetSkipNavigations().ElementAt(0);
var productSkipNavigation = productEntityType.GetSkipNavigations().ElementAt(0);

Console.WriteLine(categorySkipNavigation.Name); // "Products"
Console.WriteLine(productSkipNavigation.Name);  // "Categories"
Console.WriteLine(categorySkipNavigation.Inverse == productSkipNavigation);  // true -- 用 Inverse 还能直接拿对应的

我们知道 n-n 只是一个隐藏技巧 (背地里其实是两个 1-n),在数据库会有 3 个表,在 EntityType 层面也会有 3 个 EntityType。

var categoryEntityType = db.Model.FindEntityType(typeof(Category))!;
var productEntityType = db.Model.FindEntityType(typeof(Product))!;

var skipNavigation = categoryEntityType.GetSkipNavigations().ElementAt(0);
var joinEntityType = skipNavigation.JoinEntityType;

Console.WriteLine(joinEntityType.GetTableName()); // "CategoryProduct"
Console.WriteLine(skipNavigation.DeclaringEntityType == categoryEntityType); // true 
Console.WriteLine(joinEntityType.TargetEntityType == productEntityType); // true 

JoinEntityType 就是那个被隐藏在背后的 EntityType,它掌管着两个 1-n 关系。

ForeignKey 那些全部在 JoinEntityType 里。

var categoryEntityType = db.Model.FindEntityType(typeof(Category))!;
var categorySkipNavigation = categoryEntityType.GetSkipNavigations().ElementAt(0);

var productEntityType = db.Model.FindEntityType(typeof(Product))!;
var productSkipNavigation = productEntityType.GetSkipNavigations().ElementAt(0);

Console.WriteLine(categorySkipNavigation.ForeignKey.Properties.ElementAt(0).Name); // CategoriesId (在 CategoryProductEntityType 里)
Console.WriteLine(productSkipNavigation.ForeignKey.Properties.ElementAt(0).Name);  // ProductsId (在 CategoryProductEntityType 里)
Console.WriteLine(
  productSkipNavigation.ForeignKey.Properties.ElementAt(0).DeclaringType == productSkipNavigation.JoinEntityType // true
);

总结

好,先介绍到这里,以后有想到其它常用的再回来补上。

 

posted @ 2021-11-07 19:21  兴杰  阅读(1901)  评论(0)    收藏  举报