34

I am using ASP.NET Core 5 Web API and I am trying to use the new C# records as my model classes. But I am getting an EF Core error about tracking problems whenever I update my modified model using the with expression:

System.InvalidOperationException: The instance of entity type 'Product' cannot be
tracked because another instance with the key value '{ID: 2}' is already being
tracked. When attaching existing entities, ensure that only one entity instance
with a given key value is attached.

I believe this is due to how "mutating" records creates a new object instance and EF Core's tracking system doesn't like that, but I'm not sure the best way to fix it. Does anyone have any recommendations? Or should I go back to using regular classes instead of records?

Here's a snippet to reproduce the problem:

// Models/Product.cs
public record Product(int ID, string Name);


// Controllers/ProductController.cs
[HttpGet("test/{id}")]
public async Task<Product> ExampleControllerAction(int id, CancellationToken cancellationToken)
{
    string newName = "test new name!!";

    Product product = await db.Products.FindAsync(new object[] { id }, cancellationToken);
    product = product with { Name = newName }; // Modify the model.

    db.Update(product); // InvalidOperationException happens here.
    await db.SaveChangesAsync(cancellationToken);

    return product;
}
Phil K
  • 4,939
  • 6
  • 31
  • 56
  • 2
    What's your purpose of using record type over class? Any benefits? I don't see any benefits here. You just look like wanting to try something new but not even sure when that new thing should be used. Record types should be used in case you want the data to be immutable and adjusted over-time into ***new*** instances. Although you can define it to support mutable data, but it's then much like classes and has no benefits over classes. – King King Apr 17 '21 at 13:22
  • 8
    Immutability by default and more succinct code. But it looks like EF Core tracking isn't really compatible with immutable classes, so I guess I'll have to switch back to regular classes. – Phil K Apr 17 '21 at 13:45
  • Correct, the EF Core change tracker does not work well with records – ErikEJ Apr 17 '21 at 14:03
  • 1
    @KingKing Thanks for the comment about `with`, I totally missed that logic. – Silvermind Apr 17 '21 at 14:39
  • I believe the crux of the problem is that *with* creates a copy of the record with the changes in place. When you assign the product the new record, it loses the object reference that EF Core depends on for change tracking. – Sam Oct 27 '21 at 18:34

3 Answers3

50

From official document

Entity Framework Core depends on reference equality to ensure that it uses only one instance of an entity type for what is conceptually one entity. For this reason, record types aren't appropriate for use as entity types in Entity Framework Core.

This could confuse some people. Pay close attention to the documentation. records might not be suited for entities, but they are just fine for owned types for example, like value objects (in DDD-terms), because such values don't have a conceptual unique identity.

For example, if an Address entity, modeled as a class, owns a City value object, modeled as a record, EF would usually map City as columns inside Address like this: City_Name, City_Code, etc. (ie. record name joined with an underscore and the property name).

Notice that City has no Id, because we're not tracking unique cities here, just names and codes and whatever other information you could add to a City.

Whatever you do, don't add Ids to records and try to map them manually, because two records with the same Id don't necessarily mean the same conceptual entity to EF, probably because EF compares object instances by reference (records are ref types) and doesn't use the standard equality comparer which is different for records than for standard objects (needs confirmation).

I myself don't know how EF's works on the inside very well to talk with more certainty, but I trust the docs and you should probably trust them too, unless you want to read the source code.

Paul-Sebastian Manole
  • 2,538
  • 1
  • 32
  • 33
Mahabubul Hasan
  • 1,396
  • 13
  • 20
18

If you turn off EFs change tracking it should work as written. Try adding AsNoTracking() to the original db call.

RubberDuck
  • 11,933
  • 4
  • 50
  • 95
Mike Weiss
  • 309
  • 3
  • 11
7

Current docs state...

Reference equality is required for some data models. For example, Entity Framework Core depends on reference equality to ensure that it uses only one instance of an entity type for what is conceptually one entity. For this reason, records and record structs aren't appropriate for use as entity types in Entity Framework Core [emphasis mine].

-Records (C# reference)

...however, this does not negate Mike Weiss' AsNoTracking() alternative.

t.j.
  • 1,227
  • 3
  • 16
  • 30
  • 5
    Microsoft's official statement makes no sense... while it's true that `record` types do `override Equals`, EF and .NET has _always_ been able to bypass custom `Equals` overrides with `Object.ReferenceEquals` and go beyond with `ConditionalWeakTable` etc - so I don't know why this is apparently a problem for them... (and use of `==` in Linq expressions doesn't matter either: EF has the `Expression<>` tree object and can bind `==` to `Object.ReferenceEquals` instead of always using `static operator==`) – Dai Jun 07 '22 at 21:38