Home > OS >  Entity is still being tracked although NoTracking behaviour is set
Entity is still being tracked although NoTracking behaviour is set

Time:11-02

The dbcontext tracking behaviour is set to NoTracking, but I still get a fail in the test.

BaseDbContext in this context does not have any relevant code compared to IdentityDbContext provided by EF. Also is the same with BaseUser, where it is basically only IdentityUser.

Method for creating DbContext:

public static T GetDbContext<T>()
        where T : BaseDbContext<BaseUser<Guid>>
    {
        var optionBuilder = new DbContextOptionsBuilder<T>();
        optionBuilder.UseInMemoryDatabase(Guid.NewGuid().ToString());
        optionBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
        var obj = Activator.CreateInstance(typeof(T), optionBuilder.Options);
        if (obj == null)
        {
            throw new SystemException(typeof(T)   " was null!");
        }
        
        var ctx = (T)obj;
        ctx.Database.EnsureDeleted();
        ctx.Database.EnsureCreated();
        
        return ctx;
    }

The test, that fails:

    [Fact]
    public async void Test_UpdateSingle()
    {
        var dbContext = DbContextFactory.GetDbContext<SimpleDbContext>();
        var uow = UowFactory.GetUow<SimpleUow, SimpleDbContext>(dbContext);
        
        var id1 = Guid.NewGuid();
        var name1 = Guid.NewGuid().ToString();

        var testEntity1 = new DalSimpleEntity
        {
            Id = id1,
            Name = name1
        };

        uow.SimpleRepo.Add(testEntity1);
        await uow.SaveChangesAsync();

        var newName = Guid.NewGuid().ToString();
        testEntity1.Name = newName;

        //Fails here:
        uow.SimpleRepo.Update(testEntity1);
        await uow.SaveChangesAsync();
        Assert.Single(await uow.SimpleRepo.GetAllAsync());

        var getEntity1 = await uow.SimpleRepo.FirstOrDefaultAsync(id1);
        
        Assert.Equal(newName, getEntity1?.Name);
    }

The UnitOfWork is for using it as a layer on top of the dbcontext. The SaveChanges method calls directly the DbContext savechanges.

UnitOfWork also contains the reference for Repository.

The SimpleRepo is derived from BaseRepository. Nothing is changed in the SimpleRepo.

public abstract class BaseRepository<TDbContext, TEntityIn, TEntityOut> : BaseRepositoryWebApp<TDbContext, TEntityIn, TEntityOut, Guid>, IBaseRepositoryWebApp<TEntityOut>
    where TEntityIn : class, IDomainEntityId, IDomainEntityId<Guid>
    where TEntityOut : class, IDomainEntityId, IDomainEntityId<Guid>
    where TDbContext : BaseDbContext<BaseUser<Guid>>
{
    protected readonly DbContext RepoDbContext;
    protected readonly DbSet<TEntityIn> RepoDbSet;
    protected readonly IBaseMapper<TEntityIn, TEntityOut> Mapper;

    public BaseRepository(TDbContext dbContext, IBaseMapper<TEntityIn, TEntityOut> mapper)
    {
        RepoDbContext = dbContext;
        RepoDbSet = dbContext.Set<TEntityIn>();
        Mapper = mapper;
    }

    public virtual async Task<IEnumerable<TEntityOut>> GetAllAsync(bool noTracking = true, Guid userId = default)
    {
        var entities = await InitQuery(noTracking, userId).ToListAsync();

        return entities.Select(e => Mapper.Map(e)!);
    }

    public virtual async Task<TEntityOut?> FirstOrDefaultAsync(TKey id, bool noTracking = true, Guid userId = default)
    {
        var query = InitQuery(noTracking, userId).FirstOrDefaultAsync(e => e.Id.Equals(id));

        return Mapper.Map(await query);
    }

    public virtual async Task<bool> ExistsAsync(TKey id, Guid userId = default)
    {
        return await InitQuery(userId: userId).AnyAsync(e => e.Id.Equals(id));
    }

    public virtual async Task<TEntityOut?> RemoveAsync(TKey id, Guid userId = default)
    {
        var entity = await InitQuery(userId: userId).FirstOrDefaultAsync(e => e.Id.Equals(id));
        if (entity == null) return null;
        return Mapper.Map(RepoDbSet.Remove(entity).Entity);
    }
    
    public TEntityOut Add(TEntityOut? entity)
    {
        return Mapper.Map(RepoDbSet.Add(Mapper.Map(entity)!).Entity)!;
    }

    public TEntityOut Update(TEntityOut? entity)
    {
        return Mapper.Map(RepoDbSet.Update(Mapper.Map(entity)!).Entity)!;
    }

    protected virtual IQueryable<TEntityIn> InitQuery(bool noTracking = true, Guid userId = default)
    {
        var query = RepoDbSet.AsQueryable();

        if (typeof(IDomainEntityUsers).IsAssignableFrom(typeof(TEntityIn)))
        {
            query = query.Where(e => (e as IDomainEntityUsers)!.UserId.Equals(userId));
        }
        
        if (noTracking)
        {
            query = query.AsNoTracking();
        }

        return query;
    }
}

My question is, have I forgotten some place, where I should also state to use NoTracking behaviour?

CodePudding user response:

The problem was made difficult with the fact, that every time the entity moved down or up the layers (example: from uow to dbcontext and back, containing 2 mappings here alone). This ensured that calling

dbContext.Entity(entity).State = EntityState.Detached;

was not an option, because Savechanges was called with the uow object, not dbContext. Also in the real use case the dbContext is out of reach, thus not being able to call functions from dbContext at all.

So I searched for options to detach all objects, that were still attached after saveChanges.

ChangeTracker.Clear(); 

satisfied this requirement.

The solution for this problem is:

 public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = new())
    {
        var res = await base.SaveChangesAsync(cancellationToken);
        ChangeTracker.Clear();
        return res;
    }
  • Related