Is DbSet<>.Local something to use with special care?
Asked Answered
D

4

20

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!

Deland answered 26/2, 2013 at 16:59 Comment(4)
Even though you may not believe it provides value, providing code still may provide context to people trying to answer. Can you add a link to where you read something that it is not performing very well?Restrainer
Could you give us more details about what do you want to achieve. Examples of code with validation that you want will be useful too.Labonte
I will update the question with some code examples. here is the post about the performance (#12223791). I understand that you are not so sure about thatDeland
A first fix could be to make the Create method return the new entity.Monzonite
F
18

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).

Favourable answered 6/12, 2013 at 18:34 Comment(0)
T
7

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);
}
Tishatishri answered 22/9, 2016 at 23:6 Comment(0)
L
1

This may only apply to EF Core, but every time you reference .Local of a DbSet, you're silently triggering change detection on the context, which can be a performance hit, depending on how complex your model is, and how many entries are currently being tracked.

If this is a concern, you'll want to use (fore EFCore) dbContext.ChangeTracker.Entries<T>() to get the locally tracked entities, which will not trigger change detection, but does require manual filtering of the DB state, as it will include deleted and detached entities.

There's a similar version of this in EF6, but in EFCore the Entries is a list of EntityEntries which you'll have to select out the entry.Entity to get out the same data the DbSet would give you.

Lor answered 6/5, 2022 at 4:36 Comment(0)
P
0

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"));
Peripteral answered 15/5, 2014 at 18:29 Comment(2)
That looks like a nice concise and not-too-obtrusive workaround. Unfortunately it didn't work for me. My predicate was a bit more complex and I called Where() instead of FirstOrDefault(). I got this when accessing the result collection: An exception of type 'System.NotSupportedException' occurred in EntityFramework.dll but was not handled in user code Additional information: Unable to create a constant value of type 'Employee'. Only primitive types or enumeration types are supported in this context. So went with querying both collections separately instead.Lennyleno
Not totally sure,but I think that this could seriously degrade performance in some cases because it will pull all employees out of the database, convert them to objects, and then run your query against them. You (we) really need a system that would do: context.Employees.Where(predicate).Concat(context.Employees.Local.Where(predictae))Niels

© 2022 - 2024 — McMap. All rights reserved.