EF Core 内存数据库在测试更新操作时生成 System.InvalidOperationException

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

当我尝试使用 Entity Framework 核心测试更新操作时出现以下错误:

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.

经过一些研究,我尝试了所有发现的方法:

对于测试,我正在使用 EF 内存数据库及其固定装置,我正在使用 XUnit 和 .NET 5。 请问我可以得到任何帮助吗?

这是我的代码:

    // 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();
        }
    }

测试方法“Update_WhenCalled_UpdateACompanyObject”对我不起作用。

    // 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);
        }
    }

如果您使用的是 EF Core 5.0,则在 PrjSeedDataFixture 构造函数中的 DbContext.SaveChanges(); 之后调用 DbContext.ChangeTracker.Clear() (or go through DbContext.Entries 集合并将状态设置为 Detached。 Adding/Updating 一个条目使其被跟踪,并且您正在重用创建 ID = 1 的条目的上下文,因此当调用 _sut.Update(company); 时它将尝试再次跟踪它(因为 ReadByIdAsync 应该 return 一个未追踪的。

P.S.

围绕 EF 添加一个额外的存储库抽象层可以被视为 antipattern(因为 EF 已经实现了 repository/UoW 模式)并且您遇到的问题可以是为什么会这样的示例之一true 以及为什么这种抽象可能是有漏洞的。因此,如果您仍然认为拥有一个是个好主意 - 您需要谨慎行事。