Skip to content

Commit

Permalink
Documentation for value comparers (#2192)
Browse files Browse the repository at this point in the history
Fixes #1986
  • Loading branch information
ajcvickers committed Mar 20, 2020
1 parent 9d2f264 commit 2864d29
Show file tree
Hide file tree
Showing 8 changed files with 575 additions and 0 deletions.
148 changes: 148 additions & 0 deletions entity-framework/core/modeling/value-comparers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
---
title: Value Comparers - EF Core
description: Using value comparers to control how EF Core compares property values
author: ajcvickers
ms.date: 03/20/2020
uid: core/modeling/value-comparers
---

# Value Comparers

> [!NOTE]
> This feature is new in EF Core 3.0.
> [!TIP]
> The code in this document can be found on GitHub as a [runnable sample](https://github.com/dotnet/EntityFramework.Docs/tree/master/samples/core/Modeling/ValueConversions/).
## Background

EF Core needs to compare property values when:

* Determining whether a property has been changed as part of [detecting changes for updates](xref:core/saving/basic)
* Determining whether two key values are the same when resolving relationships

This is handled automatically for common primitive types such as int, bool, DateTime, etc.

For more complex types, choices need to be made as to how to do the comparison.
For example, a byte array could be compared:

* By reference, such that a difference is only detected if a new byte array is used
* By deep comparison, such that mutation of the bytes in the array is detected

By default, EF Core uses the first of these approaches for non-key byte arrays.
That is, only references are compared and a change is detected only when an existing byte array is replaced with a new one.
This is a pragmatic decision that avoids deep comparison of many large byte arrays when executing SaveChanges.
But the common scenario of replacing, say, an image with a different image is handled in a performant way.

On the other hand, reference equality would not work when byte arrays are used to represent binary keys.
It's very unlikely that an FK property is set to the _same instance_ as a PK property to which it needs to be compared.
Therefore, EF Core uses deep comparisons for byte arrays acting as keys.
This is unlikely to have a big performance hit since binary keys are usually short.

### Snapshots

Deep comparisons on mutable types means that EF Core needs the ability to create a deep "snapshot" of the property value.
Just copying the reference instead would result in mutating both the current value and the snapshot, since they are _the same object_.
Therefore, when deep comparisons are used on mutable types, deep snapshotting is also required.

## Properties with value converters

In the case above, EF Core has native mapping support for byte arrays and so can automatically choose appropriate defaults.
However, if the property is mapped through a [value converter](xref:core/modeling/value-conversions), then EF Core can't always determine the appropriate comparison to use.
Instead, EF Core always uses the default equality comparison defined by the type of the property.
This is often correct, but may need to be overridden when mapping more complex types.

### Simple immutable classes

Consider a property the uses a value converter to map a simple, immutable class.

[!code-csharp[SimpleImmutableClass](../../../samples/core/Modeling/ValueConversions/MappingImmutableClassProperty.cs?name=SimpleImmutableClass)]

[!code-csharp[ConfigureImmutableClassProperty](../../../samples/core/Modeling/ValueConversions/MappingImmutableClassProperty.cs?name=ConfigureImmutableClassProperty)]

Properties of this type do not need special comparisons or snapshots because:
* Equality is overridden so that different instances will compare correctly
* The type is immutable, so there is no chance of mutating a snapshot value

So in this case the default behavior of EF Core is fine as it is.

### Simple immutable Structs

The mapping for simple structs is also simple and requires no special comparers or snapshotting.

[!code-csharp[SimpleImmutableStruct](../../../samples/core/Modeling/ValueConversions/MappingImmutableStructProperty.cs?name=SimpleImmutableStruct)]

[!code-csharp[ConfigureImmutableStructProperty](../../../samples/core/Modeling/ValueConversions/MappingImmutableStructProperty.cs?name=ConfigureImmutableStructProperty)]

EF Core has built-in support for generating compiled, memberwise comparisons of struct properties.
This means structs don't need to have equality overridden for EF, but you may still choose to do this for [other reasons](/dotnet/csharp/programming-guide/statements-expressions-operators/how-to-define-value-equality-for-a-type).
Also, special snapshotting is not needed since structs immutable and are always memberwise copied anyway.
(This is also true for mutable structs, but [mutable structs should in general be avoided](/dotnet/csharp/write-safe-efficient-code).)

### Mutable classes

It is recommended that you use immutable types (classes or structs) with value converters when possible.
This is usually more efficient and has cleaner semantics than using a mutable type.

However, that being said, it is common to use properties of types that the application cannot change.
For example, mapping a property containing a list of numbers:

[!code-csharp[ListProperty](../../../samples/core/Modeling/ValueConversions/MappingListProperty.cs?name=ListProperty)]

The [`List<T>` class](/dotnet/api/system.collections.generic.list-1?view=netstandard-2.1):
* Has reference equality; two lists containing the same values are treated as different.
* Is mutable; values in the list can be added and removed.

A typical value conversion on a list property might convert the list to and from JSON:

[!code-csharp[ConfigureListProperty](../../../samples/core/Modeling/ValueConversions/MappingListProperty.cs?name=ConfigureListProperty)]

This then requires setting a `ValueComparer<T>` on the property to force EF Core use correct comparisons with this conversion:

[!code-csharp[ConfigureListPropertyComparer](../../../samples/core/Modeling/ValueConversions/MappingListProperty.cs?name=ConfigureListPropertyComparer)]

> [!NOTE]
> The model builder ("fluent") API to set a value comparer has not yet been implemented.
> Instead, the code above calls SetValueComparer on the lower-level IMutableProperty exposed by the builder as 'Metadata'.
The `ValueComparer<T>` constructor accepts three expressions:
* An expression for checking quality
* An expression for generating a hash code
* An expression to snapshot a value

In this case the comparison is done by checking if the sequences of numbers are the same.

Likewise, the hash code is built from this same sequence.
(Note that this is a hash code over mutable values and hence can [cause problems](https://ericlippert.com/2011/02/28/guidelines-and-rules-for-gethashcode/).
Be immutable instead if you can.)

The snapshot is created by cloning the list with ToList.
Again, this is only needed if the lists are going to be mutated.
Be immutable instead if you can.

> [!NOTE]
> Value converters and comparers are constructed using expressions rather than simple delegates.
> This is because EF inserts these expressions into a much more complex expression tree that is then compiled into an entity shaper delegate.
> Conceptually, this is similar to compiler inlining.
> For example, a simple conversion may just be a compiled in cast, rather than a call to another method to do the conversion.
### Key comparers

The background section covers why key comparisons may require special semantics.
Make sure to create a comparer that is appropriate for keys when setting it on a primary, principal, or foreign key property.

Use [SetKeyValueComparer](/dotnet/api/microsoft.entityframeworkcore.mutablepropertyextensions.setkeyvaluecomparer?view=efcore-3.1) in the rare cases where different semantics is required on the same property.

> [!NOTE]
> SetStructuralComparer has been obsoleted in EF Core 5.0.
> Use SetKeyValueComparer instead.
### Overriding defaults

Sometimes the default comparison used by EF Core may not be appropriate.
For example, mutation of byte arrays is not, by default, detected in EF Core.
This can be overridden by setting a different comparer on the property:

[!code-csharp[OverrideComparer](../../../samples/core/Modeling/ValueConversions/OverridingByteArrayComparisons.cs?name=OverrideComparer)]

EF Core will now compare byte sequences and will therefore detect byte array mutations.
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace EFModeling.ValueConversions
{
public class MappingImmutableClassProperty : Program
{
public void Run()
{
ConsoleWriteLines("Sample showing value conversions for a simple immutable class...");

using (var context = new SampleDbContext())
{
CleanDatabase(context);

ConsoleWriteLines("Save a new entity...");

var entity = new MyEntityType { MyProperty = new ImmutableClass(7) };
context.Add(entity);
context.SaveChanges();

ConsoleWriteLines("Change the property value and save again...");

// This will be detected and EF will update the database on SaveChanges
entity.MyProperty = new ImmutableClass(77);

context.SaveChanges();
}

using (var context = new SampleDbContext())
{
ConsoleWriteLines("Read the entity back...");

var entity = context.Set<MyEntityType>().Single();

Debug.Assert(entity.MyProperty.Value == 77);
}

ConsoleWriteLines("Sample finished.");
}

public class SampleDbContext : DbContext
{
private static readonly ILoggerFactory
Logger = LoggerFactory.Create(x => x.AddConsole()); //.SetMinimumLevel(LogLevel.Debug));

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
#region ConfigureImmutableClassProperty
modelBuilder
.Entity<MyEntityType>()
.Property(e => e.MyProperty)
.HasConversion(
v => v.Value,
v => new ImmutableClass(v));

#endregion
}

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseLoggerFactory(Logger)
.UseSqlite("DataSource=test.db")
.EnableSensitiveDataLogging();
}

public class MyEntityType
{
public int Id { get; set; }
public ImmutableClass MyProperty { get; set; }
}

#region SimpleImmutableClass
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;
}
#endregion
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace EFModeling.ValueConversions
{
public class MappingImmutableStructProperty : Program
{
public void Run()
{
ConsoleWriteLines("Sample showing value conversions for a simple immutable struct...");

using (var context = new SampleDbContext())
{
CleanDatabase(context);

ConsoleWriteLines("Save a new entity...");

var entity = new EntityType { MyProperty = new ImmutableStruct(6) };
context.Add(entity);
context.SaveChanges();

ConsoleWriteLines("Change the property value and save again...");

// This will be detected and EF will update the database on SaveChanges
entity.MyProperty = new ImmutableStruct(66);

context.SaveChanges();
}

using (var context = new SampleDbContext())
{
ConsoleWriteLines("Read the entity back...");

var entity = context.Set<EntityType>().Single();

Debug.Assert(entity.MyProperty.Value == 66);
}

ConsoleWriteLines("Sample finished.");
}

public class SampleDbContext : DbContext
{
private static readonly ILoggerFactory
Logger = LoggerFactory.Create(x => x.AddConsole()); //.SetMinimumLevel(LogLevel.Debug));

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
#region ConfigureImmutableStructProperty
modelBuilder
.Entity<EntityType>()
.Property(e => e.MyProperty)
.HasConversion(
v => v.Value,
v => new ImmutableStruct(v));
#endregion
}

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseLoggerFactory(Logger)
.UseSqlite("DataSource=test.db")
.EnableSensitiveDataLogging();
}

public class EntityType
{
public int Id { get; set; }
public ImmutableStruct MyProperty { get; set; }
}

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

public int Value { get; }
}
#endregion
}
}
Loading

0 comments on commit 2864d29

Please sign in to comment.