diff --git a/entity-framework/core/modeling/value-comparers.md b/entity-framework/core/modeling/value-comparers.md index 92c999d1c2..ca5867f7ff 100644 --- a/entity-framework/core/modeling/value-comparers.md +++ b/entity-framework/core/modeling/value-comparers.md @@ -16,43 +16,42 @@ uid: core/modeling/value-comparers ## Background -EF Core needs to compare property values when: +In EF, change tracking means that EF automatically determines what changes were performed by the application on a loaded entity instance, so that those changes can be saved back to the database when is called. EF usually performs this by taking a *snapshot* of the instance when it's loaded from the database, and *comparing* that snapshot to the instance handed out to the application. -* 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 +EF comes with built-in logic for snapshotting and comparing most standard types used in databases, so users don't usually need to worry about this topic. However, when a property is mapped through a [value converter](xref:core/modeling/value-conversions), EF Core needs to perform comparison on arbitrary user types, which may be complex. By default, EF uses the default equality comparison defined by types (e.g. the `Equals` method); for snapshotting, value types are copied to produce the snapshot, while for reference types no copying occurs, and the same instance is used as the snapshot. -This is handled automatically for common primitive types such as int, bool, DateTime, etc. +In cases where the built-in comparison behavior isn't appropriate, users may provide a *value comparer*, which contains logic for snapshotting, comparing and calculating a hash code. For example, the following sets up value conversion for `List` property to be value converted to a JSON string in the database, and defines an appropriate value comparer as well: -For more complex types, choices need to be made as to how to do the comparison. -For example, a byte array could be compared: +### [EF Core 5.0](#tab/ef5.0) -* 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 +[!code-csharp[ListProperty](../../../samples/core/Modeling/ValueConversions/MappingListProperty.cs?name=ListProperty)] + +### [Older versions](#tab/older-versions) + +[!code-csharp[ListProperty](../../../samples/core/Modeling/ValueConversions/MappingListPropertyOld.cs?name=ListProperty)] + +*** + +See [mutable classes](#mutable-classes) below for further details. + +Note that value comparers are also used when determining whether two key values are the same when resolving relationships; this is explained below. -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. +## Shallow vs. deep comparison -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. +For small, immutable value types such as `int`, EF's default logic works quite well: the value is copied as-is when snapshotted, and compared with the type's built-in equality comparison. When implementing your own value comparer, it's important to consider whether deep or shallow comparison (and snapshotting) logic is appropriate. -### Snapshots +Consider byte arrays, which can be arbitrarily large. These could be compared: -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. +* 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 -## Properties with value converters +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 copying entire arrays and comparing them byte-to-byte when executing , and the common scenario of replacing, say, one image with another is handled in a performant way. -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. +On the other hand, reference equality would not work when byte arrays are used to represent binary keys, since 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. -### Simple immutable classes +Note that the chosen comparison and snapshotting logic must correspond to each other: deep comparison requires deep snapshotting to function correctly. + +## Simple immutable classes Consider a property that uses a value converter to map a simple, immutable class. @@ -67,7 +66,7 @@ Properties of this type do not need special comparisons or snapshots because: So in this case the default behavior of EF Core is fine as it is. -### Simple immutable Structs +## Simple immutable structs The mapping for simple structs is also simple and requires no special comparers or snapshotting. @@ -77,10 +76,10 @@ The mapping for simple structs is also simple and requires no special comparers 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. +Also, special snapshotting is not needed since structs are immutable and are always copied memberwise anyway. (This is also true for mutable structs, but [mutable structs should in general be avoided](/dotnet/csharp/write-safe-efficient-code).) -### Mutable classes +## 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. @@ -90,7 +89,7 @@ For example, mapping a property containing a list of numbers: [!code-csharp[ListProperty](../../../samples/core/Modeling/ValueConversions/MappingListProperty.cs?name=ListProperty)] -The [`List` class](/dotnet/api/system.collections.generic.list-1): +The class: * Has reference equality; two lists containing the same values are treated as different. * Is mutable; values in the list can be added and removed. @@ -99,15 +98,7 @@ A typical value conversion on a list property might convert the list to and from [!code-csharp[ConfigureListProperty](../../../samples/core/Modeling/ValueConversions/MappingListProperty.cs?name=ConfigureListProperty)] -This then requires setting a `ValueComparer` 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` constructor accepts three expressions: +The constructor accepts three expressions: * An expression for checking equality * An expression for generating a hash code @@ -119,7 +110,7 @@ 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. +The snapshot is created by cloning the list with . Again, this is only needed if the lists are going to be mutated. Be immutable instead if you can. @@ -129,18 +120,17 @@ Be immutable instead if you can. > 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 +## 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) in the rare cases where different semantics is required on the same property. +Use 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. +> [!NOTE] +> has been obsoleted in EF Core 5.0. Use instead. -### Overriding defaults +## Overriding the default comparer 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. diff --git a/samples/core/Modeling/ValueConversions/MappingListProperty.cs b/samples/core/Modeling/ValueConversions/MappingListProperty.cs index 106d624879..6a9081f537 100644 --- a/samples/core/Modeling/ValueConversions/MappingListProperty.cs +++ b/samples/core/Modeling/ValueConversions/MappingListProperty.cs @@ -21,14 +21,14 @@ public void Run() ConsoleWriteLines("Save a new entity..."); - var entity = new EntityType { MyProperty = new List { 1, 2, 3 } }; + var entity = new EntityType { MyListProperty = new List { 1, 2, 3 } }; context.Add(entity); context.SaveChanges(); ConsoleWriteLines("Mutate the property value and save again..."); // This will be detected and EF will update the database on SaveChanges - entity.MyProperty.Add(4); + entity.MyListProperty.Add(4); context.SaveChanges(); } @@ -39,7 +39,7 @@ public void Run() var entity = context.Set().Single(); - Debug.Assert(entity.MyProperty.SequenceEqual(new List { 1, 2, 3, 4 })); + Debug.Assert(entity.MyListProperty.SequenceEqual(new List { 1, 2, 3, 4 })); } ConsoleWriteLines("Sample finished."); @@ -55,23 +55,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) #region ConfigureListProperty modelBuilder .Entity() - .Property(e => e.MyProperty) + .Property(e => e.MyListProperty) .HasConversion( v => JsonSerializer.Serialize(v, null), - v => JsonSerializer.Deserialize>(v, null)); - #endregion - - #region ConfigureListPropertyComparer - var valueComparer = new ValueComparer>( - (c1, c2) => c1.SequenceEqual(c2), - c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), - c => c.ToList()); - - modelBuilder - .Entity() - .Property(e => e.MyProperty) - .Metadata - .SetValueComparer(valueComparer); + v => JsonSerializer.Deserialize>(v, null), + new ValueComparer>( + (c1, c2) => c1.SequenceEqual(c2), + c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), + c => c.ToList())); #endregion } @@ -87,7 +78,7 @@ public class EntityType public int Id { get; set; } #region ListProperty - public List MyProperty { get; set; } + public List MyListProperty { get; set; } #endregion } } diff --git a/samples/core/Modeling/ValueConversions/MappingListPropertyOld.cs b/samples/core/Modeling/ValueConversions/MappingListPropertyOld.cs new file mode 100644 index 0000000000..5e3e15cbbe --- /dev/null +++ b/samples/core/Modeling/ValueConversions/MappingListPropertyOld.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.Extensions.Logging; + +namespace EFModeling.ValueConversions +{ + public class MappingListPropertyOld : Program + { + public void Run() + { + ConsoleWriteLines("Sample showing value conversions for a List..."); + + using (var context = new SampleDbContext()) + { + CleanDatabase(context); + + ConsoleWriteLines("Save a new entity..."); + + var entity = new EntityType { MyListProperty = new List { 1, 2, 3 } }; + context.Add(entity); + context.SaveChanges(); + + ConsoleWriteLines("Mutate the property value and save again..."); + + // This will be detected and EF will update the database on SaveChanges + entity.MyListProperty.Add(4); + + context.SaveChanges(); + } + + using (var context = new SampleDbContext()) + { + ConsoleWriteLines("Read the entity back..."); + + var entity = context.Set().Single(); + + Debug.Assert(entity.MyListProperty.SequenceEqual(new List { 1, 2, 3, 4 })); + } + + 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 ConfigureListProperty + modelBuilder + .Entity() + .Property(e => e.MyListProperty) + .HasConversion( + v => JsonSerializer.Serialize(v, null), + v => JsonSerializer.Deserialize>(v, null)); + + var valueComparer = new ValueComparer>( + (c1, c2) => c1.SequenceEqual(c2), + c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), + c => c.ToList()); + + modelBuilder + .Entity() + .Property(e => e.MyListProperty) + .Metadata + .SetValueComparer(valueComparer); + #endregion + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseLoggerFactory(Logger) + .UseSqlite("DataSource=test.db") + .EnableSensitiveDataLogging(); + } + + public class EntityType + { + public int Id { get; set; } + + #region ListProperty + public List MyListProperty { get; set; } + #endregion + } + } +} diff --git a/samples/core/Modeling/ValueConversions/Program.cs b/samples/core/Modeling/ValueConversions/Program.cs index d55d5b4907..2bc4edf6cc 100644 --- a/samples/core/Modeling/ValueConversions/Program.cs +++ b/samples/core/Modeling/ValueConversions/Program.cs @@ -13,6 +13,7 @@ public static void Main() new MappingImmutableClassProperty().Run(); new MappingImmutableStructProperty().Run(); new MappingListProperty().Run(); + new MappingListPropertyOld().Run(); new OverridingByteArrayComparisons().Run(); } diff --git a/samples/core/Modeling/ValueConversions/ValueConversions.csproj b/samples/core/Modeling/ValueConversions/ValueConversions.csproj index 3623fa3367..1825a97db4 100644 --- a/samples/core/Modeling/ValueConversions/ValueConversions.csproj +++ b/samples/core/Modeling/ValueConversions/ValueConversions.csproj @@ -8,8 +8,8 @@ - - + +