In our project, we need the entity to map and do some handling after it is updated.
However, it seems that the list of IEquatable objects are cleared after SaveChangesAsync, and cause our handling to become incorrect.
We expect the updated entity will remain unchanged after SaveChangesAsync
Exception message: N/A
Stack trace: N/A
```c#
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace TestEntityFrameworkCore
{
public class User
{
public Guid UserId { get; set; }
public string Email { get; set; }
public IReadOnlyList
private readonly List
public void SetRoles(IList<Role> roles)
{
if (_roles.Count == roles.Count &&
!_roles.Except(roles).Any())
return;
_roles.Clear();
_roles.AddRange(roles.Where(x => x != null).Distinct());
}
}
public class Role : IEquatable<Role>
{
public string Value { get; set; }
public bool Equals(Role other)
=> Value == other.Value;
}
public class SqlDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(@"Server=.;Database=core;Trusted_Connection=True;");
protected override void OnModelCreating(ModelBuilder builder) =>
builder.Entity<User>(m =>
{
m.ToTable("User", "user");
m.HasKey(x => x.UserId);
m.OwnsMany(x => x.Roles, nav =>
{
nav.ToTable("UserRole", "user");
nav.Property<Guid>("RoleAssignmentId");
nav.HasKey("RoleAssignmentId");
nav.Property(x => x.Value)
.HasColumnName("Role");
nav.Property<Guid>("UserId");
nav.HasForeignKey("UserId");
}).UsePropertyAccessMode(PropertyAccessMode.Field);
});
}
public class Program
{
public static async Task Main()
{
using (var context = new SqlDbContext())
{
var user = await context.Set<User>()
.Where(u => u.Email == "[email protected]")
.FirstOrDefaultAsync(CancellationToken.None);
// At this point user.Roles has 3 roles
user.SetRoles(new List<Role> {
new Role {
Value = "Basic"
}
});
// At this point user.Roles has 1 "Basic" role
await context.SaveChangesAsync(CancellationToken.None);
var role = user.Roles; // At this point user.Roles has no item
// But we expect user.Roles still have 1 "Basic" role for us to do some further handlings
}
}
}
}
```
EF Core version: 2.2.1
Database Provider: Microsoft.EntityFrameworkCore.SqlServer
Operating system: Windows 10
IDE: Visual Studio 2017 15.9.6
@jasonycw What is your expectation around implementing IEquatable on the Role entity type? Do you expect EF to treat different instances that compare equal as the same?
@ajcvickers In our case, Role is one of the ValueObject which is the one implemented IEquatable
``` C#
public class Role : ValueObject
{
public string Value { get; set; }
protected override IEnumerable
public abstract class ValueObject : IEquatable
{
...
}
```
This ValueObject is used in multiple owned entities and Role is the one we came across this issue (since it is currently the only one that needs to be handled after SaveChangesAsync)
This equal comparison is for other business logic mainly but not intended to affect EF handling.
In fact, we expect IEquatable won't affect EF handling at all as we defined HasKey("RoleAssignmentId") for the Role entity. So the key should be the one for doing comparison by EF.
The original post's sample code is simplification of our tricky scenario to demonstrate that the cause of the issue is due to us implement IEquatable for the owned entity.
Note from triage: we should look at forcing reference comparison for Contains in well-known collection types where doing so would not result in a linear search where it was not previously.
I have this problem even without implementing IEquatable, but Equals method is overriden and does not use part of the key which is a shadow property OrganizationId
var productsAvailability = await _productService.ProductsAvailability(organization);
productsAvailability.RemoveAllProducts();
productsAvailability.AddProducts(links);
// After SaveChanges collection of owned entities is empty.
// Removed and added entities have same keys, however other fields may (or may not) differ.
await Store.SaveChangesAsync();
with configuration:
internal sealed class OrganizationProductsAvailabilityConfiguration :
IEntityTypeConfiguration<OrganizationProductsAvailability>
{
public void Configure(EntityTypeBuilder<OrganizationProductsAvailability> builder)
{
builder.ToTable("OrganizationProductsAvailability");
builder.Property(_ => _.Version)
.HasDefaultValue(0)
.IsConcurrencyToken();
builder.HasKey(_ => _.OrganizationId);
builder.OwnsMany(_ => _.ProductLinks, b =>
{
b.ToTable("ProductForOrganization");
b.HasForeignKey(nameof(OrganizationProductsAvailability.OrganizationId));
b.HasKey(nameof(OrganizationProductsAvailability.OrganizationId), nameof(ProductLink.ProductId));
b.Property(m => m.AlwaysOrderedWhenOnboarding).HasDefaultValue(false);
b.HasOne<Product>()
.WithMany()
.HasForeignKey(m => m.ProductId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
});
builder.HasOne<Organization>()
.WithOne()
.HasForeignKey<OrganizationProductsAvailability>(_ => _.OrganizationId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
}
}
@voroninp If you think you are hitting a bug, then please file a new issue with a small, runnable (i.e. unlike the code you posted above we need to be able to actually execute it without guessing what else to put around it) project/solution or complete code listing. However, note that EF always uses reference equality to determine entity instance identity, and so if you are expecting EF to use some different equality then this won't work.
@ajcvickers, I am looking into the changes and I have a question:
Why are there separate checks for List and Collection in method Contains?
Why not just this:
private static bool Contains(ICollection<TElement> collection, object value)
{
switch (collection)
{
case SortedSet<TElement> sortedSet:
return sortedSet.TryGetValue((TElement)value, out var found)
&& ReferenceEquals(found, value);
default:
return collection?.Contains((TElement)value) == true;
}
}
@voroninp To force reference equality.
@ajcvickers Then why not to enumerate HashSet ? ;-) Yes, I know, it is slower, but more correct, I believe.
@voroninp Trade off. If we create a HashSet, then we create it with reference equality. If you create a HashSet, then you should do the same.
@ajcvickers It is very-very-very implicit, framework cannot enforce this requirement. People will hit this multiple times. ='(
Even your code kinda not consistent in that regard. You enforce reference equality only for List, Collection, and SortedSet, but allow "unknown" collection to use its own implementation of Contains logic.
private static bool ContainsReference(IEnumerable<TElement> sequence, object value)
{
switch (sequence)
{
case SortedSet<TElement> sortedSet:
return sortedSet.TryGetValue((TElement)value, out var found)
&& ReferenceEquals(found, value);
default:
return sequence?.Any( item => RefernceEquals(item, value)) == true;
}
}
Agreed. However, this moves us a bit closer to always using reference equality without giving up the important perf characteristics of HashSet. So its better than before, but collections and equality is still an area where things can easily go wrong.
@ajcvickers Ok, then. But at least what EF could do is emit a corresponding warning, if it detects, that Equals method of TEntity is overridden somewhere in the inheritance hierarchy and collection is not of these standard types: List, Collection or SortedSet.
@voroninp And how do we detect this in a performant and robust manner?
@ajcvickers I assume you’re building model once at cold start, therefore all types are known and fixed. Isn’t it the exact moment for this check?
Btw, this trade off ditches F# record types. :’(
@voroninp I didn't ask where to do the check, I asked _how_ to do the check.
@ajcvickers For every generic collection get the type of its elements and check whether it or any base class in the hierarchy overrdies Èquals method. Yes, it is reflection . Sure, this does not cover the case when collection's implementation of Contains does not use items's equality.
@ajcvickers , wait! Why not use same approach as with SortedSet? HashSet<T> also has this method.
@voroninp Too many false positives. Lots of classes override Equals and Contains and still do reference equality. Also, that by itself is not a good indicator that something is wrong. It's only when used with a collection that doesn't do reference equality, so we would need to detect that too,
@voroninp That might be useful for Contains, but keep in mind it's not fail-safe for SortedSet because it might not find an entity in the first place anyway. But yeah, it's a bit better.
@ajcvickers And I also think that main point of 'HashSet' is not its performance but semantics, that is set-behaviour. Set won't let me add _same_ element twice. BCL does not contain many implementations of ISet: HashSet and SortedSet. (Yes, I am annoying =P)
@voroninp I played with this in the code and I can make it work, but only if the GetHashSet and Equals are stable and consistent with reference equality. But if they are, then there is really no need to do this anyway. Also, it makes Remove less efficient since it needs to do two lookups.
The same could be said for SortedSet except that:
HashSet there is a way of constructing the HashSet that avoids all the issues, and we don't want to make that option worseSortedSet uses IComparable, which it's not abnormal to implement in in way that is compatible with reference _equality_ while still allowing ordering.Right now I'm not seeing any obvious ways to improve what we have.