【问题标题】:EF Core in-memory database generate System.InvalidOperationException when testing an update operationEF Core 内存数据库在测试更新操作时生成 System.InvalidOperationException
【发布时间】:2023-03-07 22:48:02
【问题描述】:

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

System.InvalidOperationException:无法跟踪实体类型“Companies”的实例,因为已在跟踪另一个具有键值“{Id: 1}”的实例。附加现有实体时,请确保仅附加一个具有给定键值的实体实例。

在做了一些研究之后,我尝试了所有我发现的东西:

  • 在范围数据库上下文中创建
  • 从数据库上下文中分离并附加我要更新的对象
  • 使用 "AsNoTracking()" 返回要更新的对象,我的存储库实际上是这样做的。

对于测试,我使用的是带有它的 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);
        }
    }

【问题讨论】:

    标签: c# .net entity-framework-core xunit in-memory-database


    【解决方案1】:

    如果您使用的是 EF Core 5.0,则在 PrjSeedDataFixture ctor 中的 DbContext.SaveChanges(); 之后调用 DbContext.ChangeTracker.Clear()(或通过 DbContext.Entries 集合并将状态设置为 Detached 以获取早期的)。添加/更新条目使其被跟踪,并且您正在重用创建 Id = 1 条目的上下文,因此当调用 _sut.Update(company); 时,它将尝试再次跟踪它(因为 ReadByIdAsync 应该返回未跟踪的条目)。

    附言

    在 EF 周围添加一个额外的存储库抽象层可以被视为antipattern(因为 EF 已经实现了存储库/UoW 模式),您遇到的问题可能是为什么这是真的以及为什么这种抽象可以成为一个漏水的人。因此,如果您仍然认为拥有一个是个好主意 - 您需要谨慎行事。

    【讨论】:

    • 非常感谢,在我的测试课上,在获得数据库上下文的那一刻,我使用了 ChangeTracker.Clear() 并为我工作。谢谢!
    • 关于您不将存储库模式与 EF 一起使用的建议。我想了解更多关于这方面的信息。根据我的经验:某些模式或原则与其他模式和原则相矛盾。在我们的案例中,我们考虑了来自 SOLID 的单一职责和来自 GRASP (en.wikipedia.org/wiki/GRASP_(object-oriented_design)) 的控制器原则;并且根据这些,控制器应该只处理请求和响应,因此没有空间来获取与 EF 的数据。有两个原则支持我们的想法,看起来是个好时机。这合理吗?
    • @MarcosF8 很高兴为您提供帮助!至于你的问题 - 这取决于情况。 “通常”的解决方案是在控制器和 repo 之间有一个层来封装这个逻辑。
    • @guru-stron:我试图理解,如果你说我们需要在控制器和存储库之间建立一些层,不就是存储库本身吗?所以你不能把这段代码放在Controller中,因为你不会有关注点分离,所以你需要在Controller之前有一个层,不管我们怎么称呼它,repository或不是一个需要的层,你同意吗?
    • @MarcosF8 AFAIK 将解决方案拆分为层的最流行方法之一是干净的架构 - 看看它。我通常会尝试遵循它来构建我的解决方案。
    猜你喜欢
    • 2020-04-01
    • 2021-02-16
    • 1970-01-01
    • 2023-04-10
    • 2021-04-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多