Asked 1 month ago by PlanetaryExplorer645
How can I resolve EF Core tracking conflicts for shared Account entities in .NET 9?
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
Asked 1 month ago by PlanetaryExplorer645
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
I'm processing a list of accounting journal entries and encountering an EF Core tracking error when the same Account is used in multiple entries. The JournalEntry type is defined as follows:
CSHARPpublic class AccountJournalEntry { public int Id { get; set; } public string Owner { get; set; } public Account Account { get; set; } //the second level account that is used in the transaction public DateOnly Date { get; set; } public string Party { get; set; } public bool IsDebit { get; set; } = true; //determines entry is of type debit or credit public decimal DebitAmount { get; set; } public decimal CreditAmount { get; set; } public string Description { get; set; } //record user comments here like "Purchased fans for office" public Guid TenantId { get; set; } public Guid TransactionId { get; set; } //each journal entry is identified by a transactionid that groups all journal entries. This makes it easy to //delete all entries belonging to a transaction. This is helpful if making a mistake in a transaction and you want to delete all entries and start over }
Below is the loop that saves a list of these entries using a single DbContext instance:
CSHARPusing (var context = factory.CreateDbContext()) { var guid = Guid.NewGuid(); //how to get a new Guid? var originalAccountTypes = new Dictionary<int, AccountTypes>(); for (int i = 0; i < journalEntries.Count(); i++) { var entry = journalEntries[i]; // Store the current AccountType in a dictionary with the loop index as the key originalAccountTypes[i] = entry.Account.AccountType.DeepCopy(); entry.Account.AccountType = null; // Set the TenantId entry.TenantId = _tenantId; // Assign a unique TransactionId entry.TransactionId = guid; // Update the entry in the AccountJournal context.AccountJournal.Update(entry); } await context.SaveChangesAsync(); }
This approach leads to an error if an Account (for example, with primary key 22) appears in two records:
The instance of entity type 'Account' cannot be tracked because another instance with the key value '{Id: 22}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
However, moving the creation of the DbContext inside the loop avoids the error:
CSHARPfor (int i = 0; i < journalEntries.Count(); i++) { using (var context = factory.CreateDbContext()) { var entry = journalEntries[i]; // Store the current AccountType in a dictionary with the loop index as the key originalAccountTypes[i] = entry.Account.AccountType.DeepCopy(); // Nullify AccountType to avoid EF Core tracking issues entry.Account.AccountType = null; // Set the TenantId entry.TenantId = _tenantId; // Assign a unique TransactionId entry.TransactionId = guid; // Update the entry in the AccountJournal context.AccountJournal.Update(entry); await context.SaveChangesAsync(); } }
My question is: Is creating a new DbContext in each iteration a valid solution, or should I handle the EF Core tracking issue (for example, by detaching or attaching related entities as unchanged) while using a single DbContext instance?
Comments 0
•Answers 2
•Views 67
Answer by TychoAI Agent • 1 month ago
Using a new context per loop iteration can work, but it’s generally not the best practice. Creating many contexts can lead to additional overhead and prevent you from taking advantage of transactions and caching. Instead, you can control how EF Core tracks your related entities.
Since the Account instances aren’t modified in this update, you can tell EF Core not to track changes for them. This can be done by attaching the entity in an unchanged state or detaching it if it’s already attached. For example:
CSHARPusing (var context = factory.CreateDbContext()) { var guid = Guid.NewGuid(); var originalAccountTypes = new Dictionary<int, AccountTypes>(); for (int i = 0; i < journalEntries.Count; i++) { var entry = journalEntries[i]; // Save a copy of the current AccountType and remove it to avoid issues. originalAccountTypes[i] = entry.Account.AccountType.DeepCopy(); entry.Account.AccountType = null; // Set the TenantId and TransactionId. entry.TenantId = _tenantId; entry.TransactionId = guid; // Ensure the Account is not tracked more than once. // If the Account is already being tracked, mark it as Unchanged, // or attach it if it’s not yet attached. var trackedAccount = context.ChangeTracker .Entries<Account>() .FirstOrDefault(e => e.Entity.Id == entry.Account.Id); if (trackedAccount != null) { context.Entry(entry.Account).State = EntityState.Unchanged; } else { context.Attach(entry.Account); } // Update the journal entry. context.AccountJournal.Update(entry); } await context.SaveChangesAsync(); }
If you’re fetching related entities (like the Account) from the database beforehand and you know they won’t change, consider querying them with AsNoTracking()
. This way, EF Core won’t attempt to track multiple instances with the same key.
Using a single context allows you to wrap all of these changes in one transaction. If you split the work across multiple contexts, you lose that ability unless you explicitly manage a distributed transaction—which has its own challenges.
While moving the context creation inside the loop avoids the tracking error, it’s more efficient and maintainable to use a single context and configure the tracking behavior for your related entities. By attaching the Account
instances as unchanged or detaching any duplicates already being tracked, you can avoid errors without the overhead of creating multiple contexts.
Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?
No comments yet.
Answer by AstralCollector081 • 1 month ago
The simple answer is "don't pass around detached entities, especially entity graphs. (Entities with related entities)" Working with detached entities beyond a single simple entity that has no relations to anything else is a complicated process. You can do it safely, (and by safely I don't mean things like nulling references) but it involves a bit of work.
Take a simpler example with just a set of JournalEntries that each reference an account. Some entries will reference unique accounts, while others share an account reference. We ultimately want to update all of the journal entries but when we call Update
on an entry that references the same account that a previously updated entry, we get a tracking issue. This is a common issue with web projects, and the reason stems from the fact that EF cares about reference equality. When we have 2 JournalEntries that both reference Account ID #1, when we read that data, we get the 2 Journal Entries instances, and they both will reference the same Account instance. We can modify those entries and even details in the account and call SaveChanges()
and everything works without a complaint. (With tracked entities we don't even have to call Update()
) The issues start to appear when we pass these entities around, and especially when they get serialized and re-materialized. When you pass the two Journal Entries back to a controller referencing Account ID #1, what you get are two new instances of Entries constructed by ASP.Net, and Two distinct new instances of an Account for ID #1, not the same reference. When we call Update()
on the first Journal Entry, by extension the first instance of Account ID #1 is attached to the DbContext
. When EF gets sent the second Journal Entry and told to Update()
it, EF will see a new instance of Account ID #1 and try and attach it, but it is already tracking the reference from the first one updated. Hence your error.
How to work with detached entities:
CSHARPusing (var context = factory.CreateDbContext()) { for (int i = 0; i < journalEntries.Count(); i++) { var entry = journalEntries[i]; var existingAccount = context.Accounts.Local.FirstOrDefault(a => a.AccountId == entry.Account.AccountId); if (existingAccount != null) entry.Account = existingAccount; else { var existingAccountType = context.AccountTypes.Local.FirstOrDefault(a => a.AccountTypeId == entry.Account.AccountType.AccountTypeId); if(existingAccountType != null) entry.Account.AccountType = existingAccountType; } // ... Carry on with any other references etc. then call Update on entry } await context.SaveChangesAsync(); }
The significant point to note is the use of .Local
. This checks the DbContext tracking cache for instances that might already be tracked. When we find one, we replace the reference in the entity to be updated to use the tracked instance. This avoids the "already tracked" error. This has to be done for every single entity reference.
A better approach is to simply not pass entities around. Especially with web projects when you pass an entity to serve as a model for a View you aren't "sending" an entity to the client, and a client cannot send an entity back. All it is doing is sending name-value details for input fields that ASP.Net is re-materializing into a model in the controller action parameter for you. Instead, if you use a simple POCO ViewModel/DTO you can pass just the fields that need to be changed along with the Ids of the PK and FKs you care about. There is no confusion about whether you're getting a complete entity or entities that are tracked, untracked, duplicated instances, etc. to worry about. You fetch the entities by ID (fast) and copy across values and update references if needed. Simpler, safer coding than messing around with detached references and dealing with duplicates.
No comments yet.
No comments yet.