'The instance of entity type 'Book' cannot be tracked' Error in Entity Framework and Displaying Uncommitted Changes in the UI

fatih uyanık 200 Reputation points
2025-03-09T11:06:43.0733333+00:00

Hello,

I encountered the following issue while working on my C# WPF project:

I make updates to an entity on the UI side but do not send these changes to the database. Later, when listing the entity objects I updated on the UI side, I noticed that uncommitted data was also being returned. I thought this was because the data was coming from memory, not the database. To fix this, I disabled the AsNoTracking feature in Entity Framework, and the issue seemed to go away. Given the improvement in performance and the fact that the problem was resolved, I decided to keep it this way.

However, now, when I attempt to update the data, I get the following error message:

"The instance of entity type 'Book' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values."

What would be the best approach to resolve this issue? Also, do you have any insights into why uncommitted changes are being displayed on the UI?

Thank you.

using Kütüphane_Otomasyonu.DataLayer.dataContext;
using Kütüphane_Otomasyonu.DataLayer.Interfaces;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
namespace Kütüphane_Otomasyonu.DataLayer.dataAccess
{
    public class DataAccess<TEntity> : IDataAccess<TEntity> where TEntity : class
    {
        public LibraryDataContext _context { get; }
        private readonly DbSet<TEntity> _dbSet;
        //TODO gerekli değilse silinecek.
        public bool HasChanges => _context.ChangeTracker.HasChanges();
        /// <summary>
        /// Veri tabanı işlemlerini yürütür.
        /// </summary>
        /// <param name="dataContext"> Kullanılacak veri tabanı bağlantısı</param>
        public DataAccess(LibraryDataContext dataContext)
        {
            _context = dataContext;
            _dbSet = _context.Set<TEntity>();
        }
        /// <summary>
        /// Veri tabanından tüm kayıtları getirir
        /// </summary>
        /// <returns> IEnumerable </returns>
        public async Task<IEnumerable<TEntity>> GetAllAsync()
        {
            return await _dbSet.ToListAsync();
        }
        /// <summary>
        /// Veri tabanından tüm kayıtları ilişkili tablolarla birlikte getirir.
        /// </summary>
        /// <param name="includes"> İlişkili tablolar</param>
        /// <returns> Task<IEnumerable<TEntity>> </returns>
        public async Task<IEnumerable<TEntity>> getAllAsync(params Expression<Func<TEntity, object>>[] includes)
        {
            IQueryable<TEntity> query = _dbSet;
            // Her bir include ifadesini sorguya ekle
            foreach (var include in includes)
            {
                query = query.Include(include);
            }
            return query.ToList();
        }
        /// <summary>
        /// Veri tabanında istenen idd'e göre istenen kaydı getirir.
        /// </summary>
        /// <param name="id"> Kayıt Id</param>
        /// <returns> TEntity</returns>
        public async Task<TEntity> GetByIdAsync(int id)
        {
            return await _dbSet.FindAsync(id);
        }
        /// <summary>
        /// Veri tabanına istenen kaydı ekler.
        /// </summary>
        /// <param name="entity"> Entity </param>
        /// <returns> Task </returns>
        public async Task AddAsync(TEntity entity)
        {
            await _dbSet.AddAsync(entity);
        }
        /// <summary>
        /// Veri tabanındaki istenen kaydı günceller.
        /// </summary>
        /// <param name="entity"> Entity </param>
        /// <returns> Task </returns>
        public void Update(TEntity entity)
        {
            _dbSet.Update(entity);
        }
        /// <summary>
        /// Veri tabanından istenen kaydı kaldırır. 
        /// </summary>
        /// <param name="entity"> Entity </param>
        /// <returns> task </returns>
        public async Task RemoveAsync(TEntity entity)
        {
            _dbSet.Remove(entity);
        }
        /// <summary>
        /// Veri tabanından istenen ilk kaydı getirir.
        /// </summary>
        /// <param name="predicate"> Kriter </param>
        /// <returns> TEntity</returns>
        public async Task<TEntity> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate)
        {
            return await _dbSet.FirstOrDefaultAsync(predicate);
        }
        public async Task<TEntity> FirstOrDefaultAsync()
        {
            return await _dbSet.FirstOrDefaultAsync();
        }
        public async Task<bool> AnyAsync(Expression<Func<TEntity, bool>> expression = null)
        {
            return expression == null
                ? await _dbSet.AnyAsync()
                : await _dbSet.AnyAsync(expression);
        }
        public async Task<TResult> MinAsync<TResult>(Expression<Func<TEntity, TResult>> selector)
        {
            return await _dbSet.MinAsync(selector);
        }
        public async Task<TResult> MaxAsync<TResult>(Expression<Func<TEntity, TResult>> selector)
        {
            return await _dbSet.AnyAsync()
                ? await _dbSet.MaxAsync(selector) : default;
            //return await _dbSet.MaxAsync(selector);
        }
        public async Task AddRangeAsync(IEnumerable<TEntity> entities)
        {
            await _dbSet.AddRangeAsync(entities);
        }
        public async Task<int> SaveChangesAsync()
        {
            try
            {
                return await _context.SaveChangesAsync();
            }
            catch (Exception)
            {
                throw;
            }
        }
        public async Task<int> CountAsync(Expression<Func<TEntity, bool>> expression = null)
        {
            return expression == null
                ? await _dbSet.CountAsync()
                : await _dbSet.CountAsync(expression);
        }
        public async Task<TEntity> SingleOrDefaultAsync(Expression<Func<TEntity, bool>> expression)
        {
            return await _dbSet.SingleOrDefaultAsync(expression);
        }
        public Task<TEntity> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate, params Expression<Func<TEntity, object>>[] includes)
        {
                                }
        public Task<TEntity> SingleOrDefaultAsync(Expression<Func<TEntity, bool>> predicate, params Expression<Func<TEntity, object>>[] includes)
        {
            throw new NotImplementedException();
        }
    }
}
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
11,338 questions
0 comments No comments
{count} votes

1 answer

Sort by: Most helpful
  1. Hongrui Yu-MSFT 5,010 Reputation points Microsoft External Staff
    2025-03-10T03:19:22.5133333+00:00

    Hi, @fatih uyanık. Welcome to Microsoft Q&A. 

    Reason

    AsNoTracking is generally used for read-only queries. The data queried using AsNoTracking will be in the Detached state. At this time, the queried object is the same as a normal object and is not tracked, so the database will not be affected after modification.

    The tracked data is in the Unchanged state, and when modified, it is in the Modified state. Calling SaveChanges will adjust the database for data in the Added, Modified, and Deleted states.

            var book1 = new Book() { Id = 1, Name = "Value" };
            var book2 = await dataAccess._context.Books.AsNoTracking().FirstOrDefaultAsync();
    
            var state1 = dataAccess._context.Entry(book1).State.ToString();
            var state2 = dataAccess._context.Entry(book2).State.ToString();
            /*
             *    state1:Detached    state2:Detached
             */
    
            var book3 = await dataAccess._context.Books.FirstOrDefaultAsync();
            var state3 = dataAccess._context.Entry(book3).State.ToString();
            book3.Name = "new value";
            var state4 = dataAccess._context.Entry(book3).State.ToString();
            /*
             *    state3:Unchanged    state4:Modified
             */
    
            dataAccess._context.Add(book1);
            var state5 = dataAccess._context.Entry(book1).State.ToString();
            dataAccess._context.Remove(book3);
            var state6 = dataAccess._context.Entry(book3).State.ToString();
            /*
             *    state5:Added    state6:Deleted
             */
    

    Solution

    For the data you want to modify, you could query it first, modify it, and then cancel tracking by setting State to EntityState.Detached. For read-only data, use AsNoTracking.

            var book = await dataAccess._context.Books.FirstOrDefaultAsync();
            book.Name = "new value 1";
            await dataAccess.SaveChangesAsync();
            dataAccess._context.Entry(book).State = EntityState.Detached;
    
            book.Name = "new value 2";
            await dataAccess._context.SaveChangesAsync();
            //Result: new value 1 is stored in the database, but new value 2 is not saved
    

    Why uncommitted changes are displayed on the UI

    Generally, data queried from the database is saved in ObservableCollection (this data is stored in memory). ObservableCollection is bidirectionally bound to the UI. When data is modified on the UI, the ObservableCollection (the data in memory will change accordingly) will also change accordingly. The data in the ObservableCollection is used to update the database, but the update fails, and the existing data in the database is not updated to the ObservableCollection. The ObservableCollection still uses the original data in memory to bind to the UI, so the latest data is still presented on the UI.


    If the answer is the right solution, please click "Accept Answer" and kindly upvote it. If you have extra questions about this answer, please click "Comment".

    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.


Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.