Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Static entities and cannot be tracked because another instance with the key value '{Id: }' is already being tracked. #29332

Closed
pantonis opened this issue Oct 12, 2022 · 41 comments
Labels
closed-no-further-action The issue is closed and no further action is planned. customer-reported

Comments

@pantonis
Copy link

I have an issue that I get when using static entities that exist in the database in my code. In a system using domain driven design we have static entities (we call them enum entities) where we have a lot of our business code attached to them. Problem is whenever we update an entity that has a related entity on of those enum entity we get the usual error

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

I have created a sample to demonstrate the issue. It can be found here at this repo.
AccountStatus is the enum entity in this case. When we retrieve the user and try to activate the user accounts we get this error

If we load those entities from database we don't get the error as the changetracker correctly identifies them. But whenever we are using the static instances we get this error. Since they are the same entities (same Ids) I don't understand why changetracker behaves likes this. From what I read in other ORMs this behavior is covered. Is this something that can be fixed in EF Core as well?

EF Core version: 6.0.9
Database provider: Pomelo.EntityFrameworkCore.MySql 6.0.2
Target framework: .NET 6
Operating system: Windows 10
IDE: (e.g. Visual Studio 2019 16.3) VS2022 17.3.0

@ajcvickers
Copy link
Member

@pantonis In EF7, you can use an IIdentityResolutionInterceptor. For example:

    public class MyDbContext : DbContext
    {
        private static readonly IgnoringIdentityResolutionInterceptor IgnoringIdentityResolutionInterceptor = new();

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            var connectionString = "server=127.0.0.1;port=3306;database=EfCoreDDD;uid=username;pwd=password;";
            optionsBuilder
                .AddInterceptors(IgnoringIdentityResolutionInterceptor)
                .UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
        }

@pantonis
Copy link
Author

pantonis commented Oct 12, 2022

Thanks for your reply. I have the following questions:

  1. Where is the documentation for this interceptor?
  2. Is this something that is supported on EF Core 6, since this is a production system issue we cannot wait for .NET 7 RTM
  3. Are there any examples for the solution you provided?

@ajcvickers
Copy link
Member

ajcvickers commented Oct 12, 2022

@pantonis

  1. It's not documented yet. I was having trouble coming up for a use for it which wasn't completely an anti-pattern. While I personally would never write code like you have (way too much complexity for little value) it might make a reasonable example.
  2. No
  3. Not yet. See answer to 1. There are, of course, tests.

@pantonis
Copy link
Author

way too much complexity? can you explain what is the complexity here?

@ajcvickers
Copy link
Member

way too much complexity? can you explain what is the complexity here?

The whole enum pattern with static values. But this doesn't really matter--it's just my opinion.

@pantonis
Copy link
Author

They are values that never ever change. This is just a sample of my code. Have lots of those. Save a lot of calls to database, plus they are easier to work with when you work with DDD.

@ajcvickers
Copy link
Member

Fair enough.

@pantonis
Copy link
Author

pantonis commented Oct 12, 2022

Apart from this It is still not clear to me why EF Core change tracker doesn't identify those.

@ajcvickers
Copy link
Member

Because nobody ever wrote code to do it until EF7. It's very easy to shoot yourself in the foot doing this kind of thing. The only time it's really safe is when the referenced entity type is immutable.

@pantonis
Copy link
Author

They are "immutable" Didn't add private setters in the example. Although they can be modified by adding extra modifier methods this is not the purpose of their use. In any case apart from the Interceptor workaround and the db call, is there any other way of overcoming this issue I experience?

@pantonis
Copy link
Author

pantonis commented Oct 12, 2022

And how would you resolve this scenario instead of using static entities?

@smitpatel
Copy link
Member

The core issue is that your database query is materializing instances of AccountStatus and your static fields do the same. ChangeTracker correctly identifies that there are 2 instances of same entity key value hence throws error.
Only way to avoid the error apart from using interceptor as mentioned above is to not create multiple instances. In your particular case of error, don't load AccountStatus navigation property, since you are setting it to AccountStatus.Active in all cases anyway.

@ajcvickers
Copy link
Member

You would need to perform identity resolution manually before attaching instances.

@pantonis
Copy link
Author

The core issue is that your database query is materializing instances of AccountStatus and your static fields do the same. ChangeTracker correctly identifies that there are 2 instances of same entity key value hence throws error. Only way to avoid the error apart from using interceptor as mentioned above is to not create multiple instances. In your particular case of error, don't load AccountStatus navigation property, since you are setting it to AccountStatus.Active in all cases anyway.

I can't do that since I have to check if the AccountStatus of the account is already Active first. I just used a simple scenario. In my app there is much more complicated business logic that we need the related entity loaded to check against

@smitpatel
Copy link
Member

Then as @ajcvickers mentioned you have to perform identity resolution yourself (so you don't try to attach different instance with same key) through interceptor or outside of interceptor.
Or you can try to write a materialization interceptor which rather than materializing new instance, returns your public static field value so there are no 2 instances with same key value.

@pantonis
Copy link
Author

pantonis commented Oct 12, 2022

thanks for the reply. Materialization interceptor sounds a good idea. Is this something that supported on EF7 only?

@johannbrink
Copy link

Just to emphasize @pantonis ' approach, I also use the "enum pattern with static values" approach. It is a well-known approach and is advocated on Pluralsight by Vladimir Khorikov

@pantonis
Copy link
Author

pantonis commented Oct 13, 2022

@johannbrink once again EF Core overcomplicates things unfortunately. For something so simple. Which is an antipattern the same way that TPT and TPC is an antipattern. No words honestly.

@ajcvickers ajcvickers added the closed-no-further-action The issue is closed and no further action is planned. label Oct 19, 2022
@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Oct 19, 2022
@johannbrink
Copy link

Am I missing something here? Microsoft provides documentation on the enumeration pattern here: https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/enumeration-classes-over-enum-types

So is EF Core capable of working with domain models that use enumeration classes without using this undocumented IgnoringIdentityResolutionInterceptor override?

@pantonis
Copy link
Author

@johannbrink Nice finding. I was quite surprised to see that they have an example after @ajcvickers stated that is an anti-pattern. Will try to give it a shot and let you know.

@johannbrink
Copy link

@ajcvickers any further thought after the link that I shared to the Microsoft literature above?

@ajcvickers
Copy link
Member

EF Core doesn't support tracking multiple entity instances with the same key value. If your pattern requires this, then it won't work correctly with EF Core.

any further thought after the link that I shared to the Microsoft literature above?

As I have said multiple times in the past, including when it was written, I don't agree with a lot of that documentation.

@johannbrink
Copy link

I guess it’s not one Microsoft then. Sounds more like your personal opinion is getting in the way.

@johannbrink
Copy link

@ajcvickers you are missing the point. We don’t want to track multiple entities with the same key. We want to use enumeration classes for value objects. The fact that EF insists on treating any reference type navigation property as a db table in many of the abstraction layers seem to be the source of the problem. Would be nice to creating “configurations” for these types, similar to how you do for tables when doing fluent configuration, just so that we can opt into the interceptor per type instead of globally for the whole dbcontext.

@ajcvickers
Copy link
Member

@johannbrink See #9906 for value objects. However, the enum type shown in the original code is mapped to a table and each enum object has its own Id. So it's not a value object.

@johannbrink
Copy link

@ajcvickers, I've gone and looked at it again, and think my issue was slightly different. Even though it is still related to value objects. I've logged a separate issue here: #29405

@johannbrink
Copy link

Adding 2 entities with the same tracked static value object also causes an exception

blog.Posts.Add(
    new Post { Title = "Hello World", Content = "I wrote an app using EF Core!", Status = Status.Active });
blog.Posts.Add(
    new Post { Title = "2nd post", Content = "Lets see if this works!", Status = Status.Active });//THIS BREAKS! with the following exception:  SQLite Error 19: 'NOT NULL constraint failed: Posts.Status'.
db.SaveChanges();

More info on this branch: https://github.com/johannbrink/EFCoreValueObjectPredicateExample/blob/error19/Program.cs

#Output and exception

Inserting a new blog
Querying for a blog
Updating the blog and adding a post
Unhandled exception. Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
 ---> Microsoft.Data.Sqlite.SqliteException (0x80004005): SQLite Error 19: 'NOT NULL constraint failed: Posts.Status'.
   at Microsoft.Data.Sqlite.SqliteException.ThrowExceptionForRC(Int32 rc, sqlite3 db)
   at Microsoft.Data.Sqlite.SqliteDataReader.NextResult()
   at Microsoft.Data.Sqlite.SqliteCommand.ExecuteReader(CommandBehavior behavior)
   at Microsoft.Data.Sqlite.SqliteCommand.ExecuteDbDataReader(CommandBehavior behavior)
   at System.Data.Common.DbCommand.ExecuteReader()
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReader(RelationalCommandParameterObject parameterObject)
   at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.Execute(IRelationalConnection connection)
   --- End of inner exception stack trace ---
   at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.Execute(IRelationalConnection connection)
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(IEnumerable`1 commandBatches, IRelationalConnection connection)
   at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChanges(IList`1 entries)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(IList`1 entriesToSave)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(StateManager stateManager, Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.<>c.<SaveChanges>b__104_0(DbContext _, ValueTuple`2 t)
   at Microsoft.EntityFrameworkCore.Storage.NonRetryingExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChanges()
   at Program.<Main>$(String[] args) in /Users/johann/source/sandbox/EFCoreValueObjectPredicateExample/Program.cs:line 27

@pantonis
Copy link
Author

@ajcvickers Is there any way to mark this enum entity so that change tracker stops tracking in global level? Since these kind of entities never change..

@cantti
Copy link

cantti commented Oct 24, 2022

@pantonis
You are overriding SaveChangesAsync. It seems like it does not help?

And if static entities work bad with EF Core, can someone suggest another pattern or solution?

@pantonis
Copy link
Author

It does not. Issue occurs as soon as ChangeTracker updates its state and this is done before SaveChangesAsync.

@cantti
Copy link

cantti commented Oct 25, 2022

Some alternative to static entities. It's not the same, but solves some common problems.
https://stackoverflow.com/questions/50375357/how-to-create-a-table-corresponding-to-enum-in-ef-core-code-first

@ajcvickers
Copy link
Member

Is there any way to mark this enum entity so that change tracker stops tracking in global level? Since these kind of entities never change..

Not currently; if an object is returned by a tracking query, then it is tracked. Possibly, #7586 might allow this in the future.

@pantonis
Copy link
Author

pantonis commented Oct 25, 2022

It would be good to have them. Don't understand why an entity needs to be tracked if it doesn't change? Is this something that can be done using interceptors in .NET 7 as you replied at the beginning of this post?

@ajcvickers
Copy link
Member

No, but as Smit said above, you can use a materialization interceptor to choose the instance to materialize, and hence always use your static instance.

@pantonis
Copy link
Author

@ajcvickers Is there any example or documentation of how to create and use a mat Interceptor?

@roji
Copy link
Member

roji commented Nov 13, 2022

See this section in the EF Core 7.0 What's New page.

@TomTF
Copy link

TomTF commented Jun 20, 2023

Had exactly the same issue. Fixed it (at least for my application) by adding all the static entity instances to the DbContext (within the constructor) and set their state to unchanged. This way EF won't try to track them twice.

@pantonis
Copy link
Author

I followed @ajcvickers suggestion and created an interceptor

public class EnumInterceptor : IMaterializationInterceptor
    {
        public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
        {
            if (instance is IEnumEntity enumEntity)
                materializationData.Context.Entry(enumEntity).State = EntityState.Detached;

            return instance;
        }
    }

@nillkitty
Copy link

A few questions...

@pantonis -- what was the result? How did that work for you?
@ajcvickers -- I've run into this quite a bit over the years, but my use case has always been statically caching the configuration of a server product (while other, more transient entities refer to these instances). I'd be curious what your approach to this using EF Core would be.

  • If you don't ever cache any of the instances, you're reloading the same static configuration information each time you need it (effectively bolting the entire server configuration to every primary business entity which normally references many of these configuration entities -- imagine FedEx handling many Package entities all reference the same rarely-if-ever changing entities that configure States, Countries, Postal Codes, Shipping Options, Package Options).

  • One response would be to separate the data storage duty and enumeration/validation duty into separate classes, only one of which should be an Entity; but that would effectively involve having two versions of each class (possibly re-using a DTO) merely to avoid reloading values you know didn't change or manually hooking up references.

  • Another would be for these objects to be immutable; but that complicates things even more by having to constantly supersede foreign key relationships with the replacement object instance/data

@ajcvickers
Copy link
Member

@nillkitty I don't think there is a good solution to this until #7586 is implemented.

@pantonis
Copy link
Author

@nillkitty Not very good sometimes it causes issues when updating an entity that is using it. I hope they implement #7586 soon because this small yet important issue when working with DDD causes lots of unnecessary code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed-no-further-action The issue is closed and no further action is planned. customer-reported
Projects
None yet
Development

No branches or pull requests

8 participants