Skip to content

Commit 2864d29

Browse files
authored
Documentation for value comparers (#2192)
Fixes #1986
1 parent 9d2f264 commit 2864d29

File tree

8 files changed

+575
-0
lines changed

8 files changed

+575
-0
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
---
2+
title: Value Comparers - EF Core
3+
description: Using value comparers to control how EF Core compares property values
4+
author: ajcvickers
5+
ms.date: 03/20/2020
6+
uid: core/modeling/value-comparers
7+
---
8+
9+
# Value Comparers
10+
11+
> [!NOTE]
12+
> This feature is new in EF Core 3.0.
13+
14+
> [!TIP]
15+
> 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/).
16+
17+
## Background
18+
19+
EF Core needs to compare property values when:
20+
21+
* Determining whether a property has been changed as part of [detecting changes for updates](xref:core/saving/basic)
22+
* Determining whether two key values are the same when resolving relationships
23+
24+
This is handled automatically for common primitive types such as int, bool, DateTime, etc.
25+
26+
For more complex types, choices need to be made as to how to do the comparison.
27+
For example, a byte array could be compared:
28+
29+
* By reference, such that a difference is only detected if a new byte array is used
30+
* By deep comparison, such that mutation of the bytes in the array is detected
31+
32+
By default, EF Core uses the first of these approaches for non-key byte arrays.
33+
That is, only references are compared and a change is detected only when an existing byte array is replaced with a new one.
34+
This is a pragmatic decision that avoids deep comparison of many large byte arrays when executing SaveChanges.
35+
But the common scenario of replacing, say, an image with a different image is handled in a performant way.
36+
37+
On the other hand, reference equality would not work when byte arrays are used to represent binary keys.
38+
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.
39+
Therefore, EF Core uses deep comparisons for byte arrays acting as keys.
40+
This is unlikely to have a big performance hit since binary keys are usually short.
41+
42+
### Snapshots
43+
44+
Deep comparisons on mutable types means that EF Core needs the ability to create a deep "snapshot" of the property value.
45+
Just copying the reference instead would result in mutating both the current value and the snapshot, since they are _the same object_.
46+
Therefore, when deep comparisons are used on mutable types, deep snapshotting is also required.
47+
48+
## Properties with value converters
49+
50+
In the case above, EF Core has native mapping support for byte arrays and so can automatically choose appropriate defaults.
51+
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.
52+
Instead, EF Core always uses the default equality comparison defined by the type of the property.
53+
This is often correct, but may need to be overridden when mapping more complex types.
54+
55+
### Simple immutable classes
56+
57+
Consider a property the uses a value converter to map a simple, immutable class.
58+
59+
[!code-csharp[SimpleImmutableClass](../../../samples/core/Modeling/ValueConversions/MappingImmutableClassProperty.cs?name=SimpleImmutableClass)]
60+
61+
[!code-csharp[ConfigureImmutableClassProperty](../../../samples/core/Modeling/ValueConversions/MappingImmutableClassProperty.cs?name=ConfigureImmutableClassProperty)]
62+
63+
Properties of this type do not need special comparisons or snapshots because:
64+
* Equality is overridden so that different instances will compare correctly
65+
* The type is immutable, so there is no chance of mutating a snapshot value
66+
67+
So in this case the default behavior of EF Core is fine as it is.
68+
69+
### Simple immutable Structs
70+
71+
The mapping for simple structs is also simple and requires no special comparers or snapshotting.
72+
73+
[!code-csharp[SimpleImmutableStruct](../../../samples/core/Modeling/ValueConversions/MappingImmutableStructProperty.cs?name=SimpleImmutableStruct)]
74+
75+
[!code-csharp[ConfigureImmutableStructProperty](../../../samples/core/Modeling/ValueConversions/MappingImmutableStructProperty.cs?name=ConfigureImmutableStructProperty)]
76+
77+
EF Core has built-in support for generating compiled, memberwise comparisons of struct properties.
78+
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).
79+
Also, special snapshotting is not needed since structs immutable and are always memberwise copied anyway.
80+
(This is also true for mutable structs, but [mutable structs should in general be avoided](/dotnet/csharp/write-safe-efficient-code).)
81+
82+
### Mutable classes
83+
84+
It is recommended that you use immutable types (classes or structs) with value converters when possible.
85+
This is usually more efficient and has cleaner semantics than using a mutable type.
86+
87+
However, that being said, it is common to use properties of types that the application cannot change.
88+
For example, mapping a property containing a list of numbers:
89+
90+
[!code-csharp[ListProperty](../../../samples/core/Modeling/ValueConversions/MappingListProperty.cs?name=ListProperty)]
91+
92+
The [`List<T>` class](/dotnet/api/system.collections.generic.list-1?view=netstandard-2.1):
93+
* Has reference equality; two lists containing the same values are treated as different.
94+
* Is mutable; values in the list can be added and removed.
95+
96+
A typical value conversion on a list property might convert the list to and from JSON:
97+
98+
[!code-csharp[ConfigureListProperty](../../../samples/core/Modeling/ValueConversions/MappingListProperty.cs?name=ConfigureListProperty)]
99+
100+
This then requires setting a `ValueComparer<T>` on the property to force EF Core use correct comparisons with this conversion:
101+
102+
[!code-csharp[ConfigureListPropertyComparer](../../../samples/core/Modeling/ValueConversions/MappingListProperty.cs?name=ConfigureListPropertyComparer)]
103+
104+
> [!NOTE]
105+
> The model builder ("fluent") API to set a value comparer has not yet been implemented.
106+
> Instead, the code above calls SetValueComparer on the lower-level IMutableProperty exposed by the builder as 'Metadata'.
107+
108+
The `ValueComparer<T>` constructor accepts three expressions:
109+
* An expression for checking quality
110+
* An expression for generating a hash code
111+
* An expression to snapshot a value
112+
113+
In this case the comparison is done by checking if the sequences of numbers are the same.
114+
115+
Likewise, the hash code is built from this same sequence.
116+
(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/).
117+
Be immutable instead if you can.)
118+
119+
The snapshot is created by cloning the list with ToList.
120+
Again, this is only needed if the lists are going to be mutated.
121+
Be immutable instead if you can.
122+
123+
> [!NOTE]
124+
> Value converters and comparers are constructed using expressions rather than simple delegates.
125+
> This is because EF inserts these expressions into a much more complex expression tree that is then compiled into an entity shaper delegate.
126+
> Conceptually, this is similar to compiler inlining.
127+
> For example, a simple conversion may just be a compiled in cast, rather than a call to another method to do the conversion.
128+
129+
### Key comparers
130+
131+
The background section covers why key comparisons may require special semantics.
132+
Make sure to create a comparer that is appropriate for keys when setting it on a primary, principal, or foreign key property.
133+
134+
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.
135+
136+
> [!NOTE]
137+
> SetStructuralComparer has been obsoleted in EF Core 5.0.
138+
> Use SetKeyValueComparer instead.
139+
140+
### Overriding defaults
141+
142+
Sometimes the default comparison used by EF Core may not be appropriate.
143+
For example, mutation of byte arrays is not, by default, detected in EF Core.
144+
This can be overridden by setting a different comparer on the property:
145+
146+
[!code-csharp[OverrideComparer](../../../samples/core/Modeling/ValueConversions/OverridingByteArrayComparisons.cs?name=OverrideComparer)]
147+
148+
EF Core will now compare byte sequences and will therefore detect byte array mutations.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using System.Diagnostics;
2+
using System.Linq;
3+
using Microsoft.EntityFrameworkCore;
4+
using Microsoft.Extensions.Logging;
5+
6+
namespace EFModeling.ValueConversions
7+
{
8+
public class MappingImmutableClassProperty : Program
9+
{
10+
public void Run()
11+
{
12+
ConsoleWriteLines("Sample showing value conversions for a simple immutable class...");
13+
14+
using (var context = new SampleDbContext())
15+
{
16+
CleanDatabase(context);
17+
18+
ConsoleWriteLines("Save a new entity...");
19+
20+
var entity = new MyEntityType { MyProperty = new ImmutableClass(7) };
21+
context.Add(entity);
22+
context.SaveChanges();
23+
24+
ConsoleWriteLines("Change the property value and save again...");
25+
26+
// This will be detected and EF will update the database on SaveChanges
27+
entity.MyProperty = new ImmutableClass(77);
28+
29+
context.SaveChanges();
30+
}
31+
32+
using (var context = new SampleDbContext())
33+
{
34+
ConsoleWriteLines("Read the entity back...");
35+
36+
var entity = context.Set<MyEntityType>().Single();
37+
38+
Debug.Assert(entity.MyProperty.Value == 77);
39+
}
40+
41+
ConsoleWriteLines("Sample finished.");
42+
}
43+
44+
public class SampleDbContext : DbContext
45+
{
46+
private static readonly ILoggerFactory
47+
Logger = LoggerFactory.Create(x => x.AddConsole()); //.SetMinimumLevel(LogLevel.Debug));
48+
49+
protected override void OnModelCreating(ModelBuilder modelBuilder)
50+
{
51+
#region ConfigureImmutableClassProperty
52+
modelBuilder
53+
.Entity<MyEntityType>()
54+
.Property(e => e.MyProperty)
55+
.HasConversion(
56+
v => v.Value,
57+
v => new ImmutableClass(v));
58+
59+
#endregion
60+
}
61+
62+
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
63+
=> optionsBuilder
64+
.UseLoggerFactory(Logger)
65+
.UseSqlite("DataSource=test.db")
66+
.EnableSensitiveDataLogging();
67+
}
68+
69+
public class MyEntityType
70+
{
71+
public int Id { get; set; }
72+
public ImmutableClass MyProperty { get; set; }
73+
}
74+
75+
#region SimpleImmutableClass
76+
public sealed class ImmutableClass
77+
{
78+
public ImmutableClass(int value)
79+
{
80+
Value = value;
81+
}
82+
83+
public int Value { get; }
84+
85+
private bool Equals(ImmutableClass other)
86+
=> Value == other.Value;
87+
88+
public override bool Equals(object obj)
89+
=> ReferenceEquals(this, obj) || obj is ImmutableClass other && Equals(other);
90+
91+
public override int GetHashCode()
92+
=> Value;
93+
}
94+
#endregion
95+
}
96+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using System.Diagnostics;
2+
using System.Linq;
3+
using Microsoft.EntityFrameworkCore;
4+
using Microsoft.Extensions.Logging;
5+
6+
namespace EFModeling.ValueConversions
7+
{
8+
public class MappingImmutableStructProperty : Program
9+
{
10+
public void Run()
11+
{
12+
ConsoleWriteLines("Sample showing value conversions for a simple immutable struct...");
13+
14+
using (var context = new SampleDbContext())
15+
{
16+
CleanDatabase(context);
17+
18+
ConsoleWriteLines("Save a new entity...");
19+
20+
var entity = new EntityType { MyProperty = new ImmutableStruct(6) };
21+
context.Add(entity);
22+
context.SaveChanges();
23+
24+
ConsoleWriteLines("Change the property value and save again...");
25+
26+
// This will be detected and EF will update the database on SaveChanges
27+
entity.MyProperty = new ImmutableStruct(66);
28+
29+
context.SaveChanges();
30+
}
31+
32+
using (var context = new SampleDbContext())
33+
{
34+
ConsoleWriteLines("Read the entity back...");
35+
36+
var entity = context.Set<EntityType>().Single();
37+
38+
Debug.Assert(entity.MyProperty.Value == 66);
39+
}
40+
41+
ConsoleWriteLines("Sample finished.");
42+
}
43+
44+
public class SampleDbContext : DbContext
45+
{
46+
private static readonly ILoggerFactory
47+
Logger = LoggerFactory.Create(x => x.AddConsole()); //.SetMinimumLevel(LogLevel.Debug));
48+
49+
protected override void OnModelCreating(ModelBuilder modelBuilder)
50+
{
51+
#region ConfigureImmutableStructProperty
52+
modelBuilder
53+
.Entity<EntityType>()
54+
.Property(e => e.MyProperty)
55+
.HasConversion(
56+
v => v.Value,
57+
v => new ImmutableStruct(v));
58+
#endregion
59+
}
60+
61+
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
62+
=> optionsBuilder
63+
.UseLoggerFactory(Logger)
64+
.UseSqlite("DataSource=test.db")
65+
.EnableSensitiveDataLogging();
66+
}
67+
68+
public class EntityType
69+
{
70+
public int Id { get; set; }
71+
public ImmutableStruct MyProperty { get; set; }
72+
}
73+
74+
#region SimpleImmutableStruct
75+
public readonly struct ImmutableStruct
76+
{
77+
public ImmutableStruct(int value)
78+
{
79+
Value = value;
80+
}
81+
82+
public int Value { get; }
83+
}
84+
#endregion
85+
}
86+
}

0 commit comments

Comments
 (0)