23

I have used "HasConversion" in my DBContext to define a JSonArray (Language/Value) and save it as a Text field for ages and It works like a charm, I added a new project to my solution, nothing changed but then I got a new error on adding migration regarding "setting a value comparer".

My Model is like:

    public class Brand
    {
        public int Id { get; set; }
        public new IList<LangValue> Name { get; set; } = new List<LangValue>();
    }

and DBContext is like:

    modelBuilder.Entity<Brand>(t =>
    {

        t.Property(p => p.Name).HasConversion(
            v => JsonConvert.SerializeObject(v, Formatting.Indented, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Include}),
            v => JsonConvert.DeserializeObject<IList<LangValue>>(v, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Include})
         );
    });

It was working perfectly, but after adding a new project I got Yellow error in adding migration and Model does not add to the new database.

Microsoft.EntityFrameworkCore.Model.Validation[10620] The property 'Name' on entity type 'Brand' is a collection or enumeration type with a value converter but with no value comparer. Set a value comparer to ensure the collection/enumeration elements are compared correctly.

Mertez
  • 1,061
  • 3
  • 14
  • 38

2 Answers2

27

The explanation from ValueComparer docs https://learn.microsoft.com/en-us/ef/core/modeling/value-comparers#mutable-classes

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

modelBuilder
    .Entity<EntityType>()
    .Property(e => e.MyProperty)
    .HasConversion(
        v => JsonSerializer.Serialize(v, null),
        v => JsonSerializer.Deserialize<List<int>>(v, null));

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

var valueComparer = new ValueComparer<List<int>>(
    (c1, c2) => c1.SequenceEqual(c2),
    c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
    c => c.ToList());

modelBuilder
    .Entity<EntityType>()
    .Property(e => e.MyProperty)
    .Metadata
    .SetValueComparer(valueComparer);
TheMisir
  • 4,083
  • 1
  • 27
  • 37
Seagull
  • 3,319
  • 2
  • 31
  • 37
  • 3
    Are you saying that there's no need to use `HasConversion`? I see you put in `SetValueComparer` and passed a defined `valueComparer` but I see no connection to the `HasConvertion` receiving `converter` (OP has it anonymously passed). I usually have `new ValueConverter(a => a.ToU(),b => b.ToT())`. – Konrad Viltersten Aug 16 '21 at 17:09
  • 2
    it is a seperate call as you can't chain it to the conversion. – Christian O. Apr 29 '22 at 08:09
  • 1
    You can also pass the valueComparer as a third argument to HasConversion() – Thomas Aug 02 '23 at 19:55
7

From the ValueComparer class documentation:

Specifies custom value snapshotting and comparison for CLR types that cannot be compared with Equals(Object, Object) and/or need a deep/structural copy when taking a snapshot. For example, arrays of primitive types will require both if mutation is to be detected.

Snapshotting is the process of creating a copy of the value into a snapshot so it can later be compared to determine if it has changed. For some types, such as collections, this needs to be a deep copy of the collection rather than just a shallow copy of the reference.

You can find more information on how to setup your ValueComparer on this issue:

https://github.com/dotnet/efcore/issues/17471

rdubois
  • 81
  • 4