Is DbSet<>.Local something to use with special

2019-03-18 20:05发布

For a few days now, I have been struggling with retrieving my entities from a repository (DbContext).

I am trying to save all the entities in an atomic action. Thus, different entities together represent something of value to me. If all the entities are 'valid', then I can save them all to the database. Entity 'a' is already stored in my repository, and needs to be retrieved to 'validate' entity 'b'.

That's where the problem arises. My repository relies on the DbSet<TEntity> class which works great with Linq2Sql (Include() navigation properties e.g.). But, the DbSet<TEntity> does not contain entities that are in the 'added' state.

So I have (as far as I know) two options:

  • Use the ChangeTracker to see which entities are available and query them into a set based on their EntityState.
  • Use the DbSet<TEntity>.Local property.

The ChangeTracker seems to involve some extra hard work to get it working in a way such that I can use Linq2Sql to Include() navigation properties e.g.

The DbSet<TEntity>.Local seems a bit weird to me. It might just be the name. I just read something that it is not performing very well (slower than DbSet<> itself). Not sure if that is a false statement.

Could somebody with significant EntityFramework experience shine some light on this? What's the 'wise' path to follow? Or am I seeing ghosts and should I always use the .Local property?

Update with code examples:


An example of what goes wrong

    public void AddAndRetrieveUncommittedTenant()
    {
        _tenantRepository = new TenantRepository(new TenantApplicationTestContext());

        const string tenantName = "testtenant";

        // Create the tenant, but not call `SaveChanges` yet until all entities are validated 
        _tenantRepository.Create(tenantName);

        //
        // Some other code
        //

        var tenant = _tenantRepository.GetTenants().FirstOrDefault(entity => entity.Name.Equals(tenantName));

        // The tenant will be null, because I did not call save changes yet,
        // and the implementation of the Repository uses a DbSet<TEntity>
        // instead of the DbSet<TEntity>.Local.
        Assert.IsNotNull(tenant);

        // Can I safely use DbSet<TEntity>.Local ? Or should I play 
        // around with DbContext.ChangeTracker instead?
    }

An example of how I want to use my Repository

In my Repository I have this method:

    public IQueryable<TEntity> GetAll()
    {
        return Context.Set<TEntity>().AsQueryable();
    }

Which I use in business code in this fashion:

    public List<Case> GetCasesForUser(User user)
    {
        return _repository.GetAll().
            Where(@case => @case.Owner.EmailAddress.Equals(user.EmailAddress)).
            Include(@case => @case.Type).
            Include(@case => @case.Owner).
            ToList();
    }

That is mainly the reason why I prefer to stick to DbSet like variables. I need the flexibility to Include navigation properties. If I use the ChangeTracker I retrieve the entities in a List, which does not allow me to lazy load related entities at a later point in time.

If this is close to incomprehensible bullsh*t, then please let me know so that I can improve the question. I desperately need an answer.

Thx a lot in advance!

3条回答
男人必须洒脱
2楼-- · 2019-03-18 20:26

As mentioned by Terry Coatta, the best approach if you don't want to save the records first would be checking both sources.

For example:

public Person LookupPerson(string emailAddress, DateTime effectiveDate)
{
    Expression<Func<Person, bool>> criteria = 
        p =>
            p.EmailAddress == emailAddress &&
            p.EffectiveDate == effectiveDate;

    return LookupPerson(_context.ObjectSet<Person>.Local.AsQueryable(), criteria) ?? // Search local
           LookupPerson(_context.ObjectSet<Person>.AsQueryable(), criteria); // Search database
}

private Person LookupPerson(IQueryable<Person> source, Expression<Func<Person, bool>> predicate)
{
    return source.FirstOrDefault(predicate);
}
查看更多
一纸荒年 Trace。
3楼-- · 2019-03-18 20:33

If you want to be able to 'easily' issue a query against the DbSet and have it find newly created items, then you will need to call SaveChanges() after each entity is created. If you are using a 'unit of work' style approach to working with persistent entities, this is actually not problematic because you can have the unit of work wrap all actions within the UoW as a DB transaction (i.e. create a new TransactionScope when the UoW is created, and call Commit() on it when the UoW completed). With this structure, the changes are sent to the DB, and will be visible to DbSet, but not visible to other UoWs (modulo whatever isolation level you use).

If you don't want the overhead of this, then you need to modify your code to make use of Local at appropriate times (which may involve looking at Local, and then issuing a query against the DbSet if you didn't find what you were looking for). The Find() method on DbSet can also be quite helpful in these situations. It will find an entity by primary key in either Local or the DB. So if you only need to locate items by primary key, this is pretty convenient (and has performance advantages as well).

查看更多
地球回转人心会变
4楼-- · 2019-03-18 20:40

For those who come after, I ran into some similar issues and decided to give the .Concat method a try. I have not done extensive performance testing so someone with more knowledge than I should feel free to chime in.

Essentially, in order to properly break up functionality into smaller chunks, I ended up with a situation in which I had a method that didn't know about consecutive or previous calls to that same method in the current UoW. So I did this:

var context = new MyDbContextClass();
var emp = context.Employees.Concat(context.Employees.Local).FirstOrDefault(e => e.Name.Contains("some name"));
查看更多
登录 后发表回答