值比较器

提示

此代码可在 GitHub 上找到(作为可运行示例)。

背景

更改跟踪意味着 EF Core 会自动确定应用程序对加载的实体实例所执行的更改,以便在调用 SaveChanges 时可以将这些更改保存回数据库。 EF Core 通常通过在从数据库加载实例时截取实例的快照并将该快照与分发给应用程序的实例进行比较来执行此操作

EF Core 内置有用于快照截取和比较数据库中使用的大多数标准类型的逻辑,所以用户通常不需要担心这个问题。 但是,当通过值转换器映射属性时,EF Core 需要对任意用户类型执行比较,这可能很复杂。 默认情况下,EF Core 使用由类型定义的默认相等比较(例如 Equals 方法);对于快照截取,将复制值类型以生成快照,而对于引用类型,则不进行复制,并将同一实例用作快照。

如果内置比较行为不合适,用户可以提供值比较器,其中包含用于快照截取、比较和计算哈希代码的逻辑。 例如,下面将 List<int> 属性的值转换设置为将值转换为数据库中的 JSON 字符串,并定义适当的值比较器:

modelBuilder
    .Entity<EntityType>()
    .Property(e => e.MyListProperty)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
        v => JsonSerializer.Deserialize<List<int>>(v, (JsonSerializerOptions)null),
        new ValueComparer<List<int>>(
            (c1, c2) => c1.SequenceEqual(c2),
            c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
            c => c.ToList()));

有关更多详细信息,请参阅下面的可变类

请注意,在解析关系时确定两个键值是否相同时,也会使用值比较器;下面对此进行了说明。

浅层比较与深层比较

对于小的、不可变的值类型,如 int,EF Core 的默认逻辑十分适用:在截取快照时,值将按原样复制,并使用类型的内置相等比较进行比较。 在实现自己的值比较器时,请务必考虑深层或浅层比较(和快照截取)逻辑是否合适。

考虑字节数组,它可以是任意大小。 可按以下方法进行比较:

  • 通过引用,只有在使用新的字节数组时才会检测差异
  • 通过深层比较,可以检测到数组中字节的转变

默认情况下,EF Core 使用第一种方法来处理非键字节数组。 也就是说,只有当现有字节数组被新数组替换时,才会比较引用并检测更改。 这是一项务实的决策,避免了在执行 SaveChanges 时复制整个数组并对它们进行逐字节比较。 这意味着常见替换场景(即将一个映像替换为另一个)以一种高效方式进行处理。

另一方面,当使用字节数组表示二进制键时,引用相等性将不起作用,因为 FK 属性不太可能设置为需与之进行比较的 PK 属性的相同实例。 因此,EF Core 对充当键的字节数组使用深层比较;这不太可能对性能造成很大的影响,因为二进制键通常很短。

请注意,所选的比较和快照截取逻辑必须相互对应:深层比较需要深度快照截取才能正常工作。

简单的不可变类

考虑一个使用值转换器映射简单的不可变类的属性。

public sealed class ImmutableClass
{
    public ImmutableClass(int value)
    {
        Value = value;
    }

    public int Value { get; }

    private bool Equals(ImmutableClass other)
        => Value == other.Value;

    public override bool Equals(object obj)
        => ReferenceEquals(this, obj) || obj is ImmutableClass other && Equals(other);

    public override int GetHashCode()
        => Value.GetHashCode();
}
modelBuilder
    .Entity<MyEntityType>()
    .Property(e => e.MyProperty)
    .HasConversion(
        v => v.Value,
        v => new ImmutableClass(v));

此类型的属性不需要特殊比较或快照,原因如下:

  • 相等性被覆盖,以便不同的实例可以正确比较
  • 类型是不可变的,所以不可能改变快照值

因此,在这种情况下,EF Core 的默认行为本身就是正常的。

简单的不可变结构

简单结构的映射也很简单,不需要特殊的比较器或快照截取。

public readonly struct ImmutableStruct
{
    public ImmutableStruct(int value)
    {
        Value = value;
    }

    public int Value { get; }
}
modelBuilder
    .Entity<EntityType>()
    .Property(e => e.MyProperty)
    .HasConversion(
        v => v.Value,
        v => new ImmutableStruct(v));

EF Core 内置有对生成结构属性的已编译、按成员比较的支持。 这意味着结构不需要为 EF Core 覆盖相等性,但你仍然可以出于其他原因选择这样做。 此外,不需要特殊的快照截取,因为结构是不可变的,并且始终按成员复制。 (对于可变结构也是如此,但通常应避免使用可变结构。)

可变类

建议尽可能将不可变类型(类或结构)与值转换器一起使用。 这通常比使用可变类型更有效,语义更清晰。 但是,话虽如此,使用应用程序无法更改的类型的属性是很常见的。 例如,映射包含数字列表的属性:

public List<int> MyListProperty { get; set; }

List<T> 类:

  • 具有引用相等性;包含相同值的两个列表被视为不同。
  • 是可变的;可以添加和删除列表中的值。

列表属性上的典型值转换可能会在列表与 JSON 之间进行转换:

modelBuilder
    .Entity<EntityType>()
    .Property(e => e.MyListProperty)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
        v => JsonSerializer.Deserialize<List<int>>(v, (JsonSerializerOptions)null),
        new ValueComparer<List<int>>(
            (c1, c2) => c1.SequenceEqual(c2),
            c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
            c => c.ToList()));

ValueComparer<T> 构造函数接受三个表达式:

  • 用于检查相等性的表达式
  • 用于生成哈希代码的表达式
  • 用于截取值的快照的表达式

在这种情况下,比较是通过检查数字序列是否相同来完成的。

同样,哈希代码也是基于相同的序列构建的。 (请注意,这是一个基于可变值的哈希代码,因此可能导致问题。如果可以,请改为不可变。)

快照是通过使用 ToList 克隆列表来创建的。 同样,仅当要转变列表时,才需要这样做。 如果可以,请改为不可变。

注意

值转换器和比较器是使用表达式而不是简单的委托来构造的。 这是因为 EF Core 将这些表达式插入到更复杂的表达式树中,然后将其编译为实体整形器委托。 从概念上讲,这类似于编译器内联。 例如,一个简单的转换可能只是在强制转换中编译,而不是调用另一个方法来执行转换。

键比较器

背景部分介绍了键比较可能需要特殊语义的原因。 在主、主体或外键属性上设置键时,请确保创建适合键的比较器。

在同一属性需要不同语义的极少数情况下,请使用 SetKeyValueComparer

注意

SetStructuralValueComparer 已过时。 请改用 SetKeyValueComparer

重写默认比较器

有时 EF Core 使用的默认比较可能不合适。 例如,默认情况下,不会在 EF Core 中检测到字节数组的转变。 这可以通过对属性设置不同的比较器来重写:

modelBuilder
    .Entity<EntityType>()
    .Property(e => e.MyBytes)
    .Metadata
    .SetValueComparer(
        new ValueComparer<byte[]>(
            (c1, c2) => c1.SequenceEqual(c2),
            c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
            c => c.ToArray()));

EF Core 现在将比较字节序列,因此将检测字节数组转变。