Use correct value comparers for collection properties with value conversion

See original GitHub issue

My entity has a collection of SmartEnums (simplified in the code example below) which is persisted in the database using a ValueConverter in order to keep the domain model clean (I wanted to avoid introducing a wrapper entity with an extra ID property). It works correctly for retrieving data but unfortunately doesn’t seem to pick up changes to the collection automatically. When calling SaveChanges, the changes are not persisted unless the entity state is manually set to EntityState.Modified beforehand.

Steps to reproduce

MySmartEnum

public class MySmartEnum
    {
        public string Value { get; set; }

        public static MySmartEnum FromValue(string value)
        {
            return new MySmartEnum { Value = value };
        }
    }

Entity

public class Entity
    {
        public Entity()
        {
            SmartEnumCollection = new HashSet<MySmartEnum>();
        }

        public int Id { get; set; }
        public virtual ICollection<MySmartEnum> SmartEnumCollection { get; set; }
    }

Context

public class TestContext : DbContext
    {
        public TestContext()
        { }

        public TestContext(DbContextOptions<TestContext> options)
            : base(options)
        { }

        public virtual DbSet<Entity> Entities { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            var valueConverter = new ValueConverter<ICollection<MySmartEnum>, string>
            (
                e => string.Join(',', e.Select(c => c.Value)),
                str => new HashSet<MySmartEnum>(
                            str.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
                               .Select(x => MySmartEnum.FromValue(x)))
            );

            modelBuilder.Entity<Entity>()
                .Property(e => e.SmartEnumCollection)
                .HasConversion(valueConverter);
        }
    }

Update method

using (var context = new TestContext(options))
            {
                var entity = context.Entities
                    .First();

                entity.SmartEnumCollection.Add(MySmartEnum.FromValue("Test"));

                // Changes are persisted only if the following line is uncommented:
                //context.Entry(entity).State = EntityState.Modified;

                context.SaveChanges();
            }

Further technical details

EF Core version: 3.0.0-rc1.19427.8 Database Provider: Microsoft.EntityFrameworkCore.SqlServer (Version 3.0.0-rc1.19427.8) Operating system: Windows 10 (Version 10.0.18362.295) IDE: Visual Studio 2019 16.2.3

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:5
  • Comments:9 (4 by maintainers)

github_iconTop GitHub Comments

19reactions
ajcvickerscommented, Aug 29, 2019

@BalintBanyasz The issue here is that EF Core needs to be able to create a snapshot of the current value and then compare that snapshot with the new value to see if it has changed. This requires special code when using value conversion to map a type with significant structure–in this case the ICollection<>.

The fix for this is to tell EF how to snapshot and compare these collections. For example:

var valueComparer = new ValueComparer<ICollection<MySmartEnum>>(
    (c1, c2) => c1.SequenceEqual(c2), 
    c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
    c => (ICollection<MySmartEnum>)c.ToHashSet());

modelBuilder.Entity<Entity>()
    .Property(e => e.SmartEnumCollection)
    .HasConversion(valueConverter)
    .Metadata.SetValueComparer(valueComparer);

Notes for triage: we should consider:

4reactions
ajcvickerscommented, Aug 30, 2019

Note from triage:

  • Warn about this case in 3.1
  • Consider shipping well-known specific value comparers
  • In future, decide if we should do this automatically and also:
    • Can we detect IEquatable or IComparable and use it, only generating the snapshotting?
Read more comments on GitHub >

github_iconTop Results From Across the Web

Value Comparers - EF Core
Using value comparers to control how EF Core compares property values. ... up value conversion for List<int> property to be value converted ......
Read more >
Set a value comparer in ASP.NET Core 3.1
Set a value comparer to ensure the collection/enumeration elements are compared correctly.
Read more >
Set a value comparer in ASP.NET Core 3.1
The property 'Name' on entity type 'Brand' is a collection or enumeration type with a value converter but with no value comparer. Set...
Read more >
Entity Framework Core: Improved Value Conversion Support
Entity Framework Core (EF) 2.1 introduced a new feature called Value Conversion. Now, we are able to map custom .NET types to a...
Read more >
Equality comparisons and sameness - JavaScript | MDN
Strict equality compares two values for equality. Neither value is implicitly converted to some other value before being compared. If the values ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found