“Local” Queries


Quite some time ago I wrote a blog post about the fact that EF queries execute at the database rather than locally which means that if you add an object to the context or you modify an object already attached to the context, then a query won’t be based on the local data.


Earlier today another blogger called this a “disappointing behavior” .  Since I hate for the EF to disappoint folks, I thought I’d whip up a small extension method to help:


public static IEnumerable<T> Local<T>(this ObjectContext context,


                                      string entitySetName) where T : class


{


    return from stateEntry in context.ObjectStateManager


                                     .GetObjectStateEntries(EntityState.Added |                            


                                                            EntityState.Modified |


                                                            EntityState.Unchanged)


           where stateEntry.Entity != null && stateEntry.EntitySet.Name == entitySetName


           select stateEntry.Entity as T;


}


To use this, you just need to supply the type and the entity set name:


foreach (Customer c in ctx.Local<Customer>(“Customers”))


{


    Console.WriteLine(c.ContactName);


}


In .Net 4.0 we will be adding a new type ObjectSet<T> which inherits from ObjectQuery<T> and provides methods for things like adding, attaching, deleting, etc. entities to the entity set which it represents.  The default codegen will put instances of ObjectSet<T> on the context rather than ObjectQuery<T>.  So, once .Net 4.0 ships we can change the extension method to:


public static IEnumerable<T> Local<T>(this ObjectSet<T> objectSet) where T : class


{


    return from stateEntry in objectSet.Context.ObjectStateManager


                                       .GetObjectStateEntries(EntityState.Added |                                   


                                                              EntityState.Modified |


                                                              EntityState.Unchanged)


           where stateEntry.Entity != null && stateEntry.EntitySet == objectSet.EntitySet


           select stateEntry.Entity as T;


}


And then you won’t need to specify either the type or the entity set name but rather call the extension method on the object set so that you get a nicer usage pattern:


foreach (Customer c in ctx.Customers.Local())


{


    Console.WriteLine(c.ContactName);


}


– Danny

Comments (10)

  1. Danny Simmons posted small helper method for runnning queries against local cache (in ObjectContext).

  2. JowenMei says:

    Hey Danny

    Is checking the entitySet name really necessary?

    Isn’t comparing the type sufficient?

    if (entry.Entity != null && entry.Entity.GetType() == typeof(T))

    {

        entries.Add((T)entry.Entity);

    }

    regards, Jowen

  3. simmdan says:

    @Jowen,

    For many scenarios comparing against the type is sufficient, but it’s not really sufficient in all cases.  The issue is that the EF supports the ability to have multiple entitysets for the same type.  Normally the wizard which generates models from the database and the EF designer will not ever produce a model with more than one entityset for the same type, but they can be produced by hand, and in some scenarios are very important.

    So the extensions I’ve written above work on any EF model, even one with multiple entitysets for the same type, but you could change the extension to be like you describe and thus get rid of the need to pass an entityset name in as an argument–as long as you are sure that you will always only run with models which have one entityset pre type.

    – Danny

  4. JowenMei says:

    Ok, thats what I thought. Tnx for the clear explanation!

  5. Frederic Ouellet says:

    The extension method Local() will still be required in 4.0 ?

    The ObjectSet won’t include local changes (like added entities) ?

    If it allow add, remove operations, I thought it would consider the local changes for future queries.  I also find this a disappointing behavior.

    Thanks.

  6. simmdan says:

    @Frederic,

    Yes, it will still be required in 4.0.  The addition of ObjectSet is just a way to simplify some programming patterns without changing the underlying programming model of the EF.  

    The ObjectContext is not a cache.  Yes, it’s possible to execute some queries locally, but there’s not a good way to automatically decide when to execute them locally and when to do so remotely.  If enumerating the ObjectSet only returned local objects all the time, then how would you load the entities in the first place?  Would it load from the store the first time?  If so, then how would you retrieve additional entities added later?

    There could be solutions to each of these, but at the end of the day I think it comes down to the fact that you need to explicitly indicate what you want.

    – Danny

  7. Frederic Ouellet says:

    Thanks for your answer.

    What I find incoherent is:

    If you use query (default MergeOption) you get the current values of modified / deleted entities, but you don`t see the one added.  You are half local, half remote.  

    Why isnt there a MergeOption that allow you to see the locally added entities, like it is possible to see the current modified values ?  What I would have expected is that added entities that fit the query would also be returned.  I am concerned about performance if we need to filter out added entities from the state manager, there could be a lot of entities there all mixed up(types)…

    Again thanks for your answer.

  8. simmdan says:

    @Frederic,

    The default MergeOption (AppendOnly) gives the standard identity resolution pattern which is common to many object/relational systems.  This is a little counter-intuitive at first, but there is a lot to be said for it when you get used to it.

    One reason we can’t create a merge option for local queries is that merge option doesn’t affect the way the query is executed–only the way that values are merged with entities already found in the context (or not merged at all in the case of NoTracking).  The merge option isn’t even considered until after the query has been executed.

    Another reason is that not every kind of query that can be executed against the database can be executed locally.  Locally all we have is LINQ to Objects.  We don’t have Entity SQL or the various kinds of database functions and things which are available on the server.  Further, some aspects of LINQ to Objects semantics are different from the way queries run on the server (things like the handling of nulls come to mind).

    So the Local mechanism above can be useful for some cases, but it’s not interchangeable with real database queries which is why we want to explicitly opt-in to it when appropriate.

    – Danny

  9. Pat says:

    Would further queries against Customers.Local() make trips to the database (assuming LazyLoading is disabled)? For example, would Customers.Local().Addresses.ToList() (assuming there's a navigation property to an address table) just look within the local customer's data? I would assume that it would, and if LazyLoading is disabled you might get an exception if you haven't manually loaded the address data. Is that correct?