Search code examples
c#.netentity-framework-corexunitin-memory-database

EF Core in-memory database generate System.InvalidOperationException when testing an update operation


I got the following error when I try to test an update operation using Entity Framework core:

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

After doing some research, I tried everything that I found:

  • Create in scope DB context
  • deattach and attached the object I want to update from the DB context
  • Return the object to be updated using "AsNoTracking()" , my repository actually do this.

For the testing I am using EF in-memmory database with it fixture, I am using XUnit and .NET 5. Can I get any help with this please?

Here is my code:

    // The repository I am trying to test
    public class RepositoryBase<T> : ICrudRepository<T> where T : class, IModel
    {
        protected PrjDbContext DatabaseContext { get; set; }

        public RepositoryBase(PrjDbContext databaseContext) => DatabaseContext = databaseContext;

        protected IQueryable<T> FindAll() => DatabaseContext.Set<T>().AsNoTracking();

        protected IQueryable<T> FindBy(Expression<Func<T, bool>> expression) => DatabaseContext.Set<T>().Where(expression).AsNoTracking();

        public void Create(T entity) => DatabaseContext.Set<T>().Add(entity);

        public void Update(T entity) => DatabaseContext.Set<T>().Update(entity);

        public void Delete(T entity) => DatabaseContext.Set<T>().Remove(entity);

        public async Task<IEnumerable<T>> ReadAllAsync() => await FindAll().ToListAsync().ConfigureAwait(false);

        public async Task<T> ReadByIdAsync(int id) => await FindBy(entity => entity.Id.Equals(id)).FirstOrDefaultAsync().ConfigureAwait(false);
    }

    //The Database context  
    public partial class PrjDbContext : DbContext
    {
        public PrjDbContext()
        {
            
        }

        public PrjDbContext(DbContextOptions<PrjDbContext> options)
            : base(options)
        {
            
        }

        public virtual DbSet<Companies> Companies { get; set; }
       
    }  

    // This is my fixture with the in-memory Database 
    public sealed class PrjSeedDataFixture : IDisposable
    {
        public PrjDbContext DbContext { get; }

        public PrjSeedDataFixture(string name)
        {
            string databaseName = "PrjDatabase_" + name + "_" + DateTime.Now.ToFileTimeUtc();
            DbContextOptions<PrjDbContext> options = new DbContextOptionsBuilder<PrjDbContext>()
                .UseInMemoryDatabase(databaseName)
                .EnableSensitiveDataLogging()
                .Options;

            DbContext = new PrjDbContext(options);

            // Load Companies
            DbContext.Companies.Add(new Companies { Id = 1, Name = "Customer 1", Status = 0, Created = DateTime.Now, LogoName = "FakeLogo.jpg", LogoPath = "/LogoPath/SecondFolder/", ModifiedBy = "Admin" });
            DbContext.Companies.AsNoTracking();

            DbContext.SaveChanges();
        }

        public void Dispose()
        {
            DbContext.Dispose();
        }
    }

The test method "Update_WhenCalled_UpdateACompanyObject", is not working for me.

    // And finally, this is my test class, Create_WhenCalled_CreatesNewCompanyObject pass the test, but Update_WhenCalled_UpdateACompanyObject isn't passing the test.
    public class RepositoryBaseCompanyTests
    {
        private Companies _newCompany;
        private PrjDbContext _databaseContext;
        private RepositoryBase<Companies> _sut;
        
        public RepositoryBaseCompanyTests()
        {
            _newCompany = new Companies {Id = 2};
            _databaseContext = new PrjSeedDataFixture("RepositoryBase").DbContext;
            _sut = new RepositoryBase<Companies>(_databaseContext);
        }

        [Fact]
        public void Create_WhenCalled_CreatesNewCompanyObject()
        {
            //Act
            _sut.Create(_newCompany);
            _databaseContext.SaveChanges();

            //Assert
            Assert.Equal(2, _databaseContext.Companies.Where( x => x.Id == 2).FirstOrDefault().Id);
            
        }

        [Fact]
        public async void Update_WhenCalled_UpdateACompanyObject()
        {
            //Arrange
            var company = await _sut.ReadByIdAsync(1);
            company.Name = "Customer 2";
            //_databaseContext.Entry(company).State = EntityState.Detached;
            //_databaseContext.Attach(company);
            //_databaseContext.Entry(company).State = EntityState.Modified;

            //Act
            _sut.Update(company);
            await _databaseContext.SaveChangesAsync();

            //Assert
            Assert.Equal("Customer 2", _databaseContext.Companies.Where(x => x.Id == 1).FirstOrDefault().Name);
        }
    }

Solution

  • If you are using EF Core 5.0 then call DbContext.ChangeTracker.Clear() (or go through DbContext.Entries collection and set state to Detached for earlier ones) after DbContext.SaveChanges(); in PrjSeedDataFixture ctor. Adding/Updating an entry makes it tracked and you are reusing the context that created an entry with Id = 1, so when _sut.Update(company); is called it will try to track it again (since ReadByIdAsync should return an untracked one).

    P.S.

    Adding an extra repository abstraction layer around EF can be considered as antipattern (because EF already implements repository/UoW patterns) and the issue you are having can be one of the examples of why that is true and why this abstraction can be a leaky one. So if you still decide that having one is a good idea - you need to proceed with caution.