Exception when using Cached and NotCached

Oct 8, 2014 at 11:57 AM
When trying to use the Cached or NotCached extension methods, I'm getting an exception in TryGetObjectQuery. The problem seems to be that source.GetType().GetField("_internalQuery", privateFieldFlags) is null (i.e. the _internalQuery field does not exist on the DbQuery instance.

I'm using EF 6.1.0.
Oct 8, 2014 at 1:15 PM
Actually, I'm using EF 6.1.1; not sure if this is a problem with EF 6.1.0 too.

Anyway, This can be fixed by changing TryGetObjectQuery to retrieve the internal fields from the base type:
        private static ObjectQuery TryGetObjectQuery<T>(IQueryable<T> source)
        {
            var dbQuery = source as DbQuery<T>;
            
            if (dbQuery != null)
            {
                const BindingFlags privateFieldFlags = BindingFlags.NonPublic | BindingFlags.Instance;

                var internalQuery =
                    source.GetType().BaseType.GetField("_internalQuery", privateFieldFlags)
                        .GetValue(source);

                return
                    (ObjectQuery)internalQuery.GetType().BaseType.GetField("_objectQuery", privateFieldFlags)
                        .GetValue(internalQuery);
            }

            return null;
        }
Marked as answer by cocowalla on 10/8/2014 at 5:15 AM
Oct 14, 2014 at 6:41 PM
Thanks for reporting this. I will take a look. (This is the most obscure/hacky code in the entire EFCache and I expected there would be some problems I did not anticipate but could not find a better way).

Thanks,
Pawel
Oct 16, 2014 at 10:02 AM
Edited Oct 16, 2014 at 11:38 AM
Actually, the problem is that there is a difference between applying the Cached extension method to a DbSet and to a DbQuery. (Of course the same goes for NotCached as well.)

Let's take the following examples:
var people = dbContext.People.Cached().ToList(); // DbSet
var people2 = dbContext.People.Where(person => person.Name.StartsWith("A")).Cached().ToList(); // DbQuery
The original Cached extension method works only for the DbQuery case; while the other version - proposed by cocowalla - would work only for the DbSet case, except that it has some bugs in it.

The main problem is - that has been described by cocowalla as well - that although a DbSet is a DbQuery as well (since DbSet inherits from DbQuery), yet we have to give the DbQuery type to the GetField method if we want to access the _internalQuery field.

However, in case of a DbSet the internalQuery parameter (with actual type InternalQuery) that is given to the DbQuery constructor is actually the DbSet's _internalSet field (with actual type InternalSet which inherits from InternalQuery).

Now, in case of an InternalQuery type accessing its _objectQuery would be enough, however, in case of an InternalSet type it would return null unless its ObjectQuery property has been accessed at least once, thus calling its Initialize method which actually fills up the _objectQuery field. (Note that even just observing the _internalSet field in Visual Studio's watch windows during debugging is enough for the ObjectQuery property being called.)

The proper solution would be something like this:
private static ObjectQuery TryGetObjectQuery<T>(IQueryable<T> source)
    where T : class
{
    var dbQuery = source as DbQuery<T>;

    if (dbQuery == null)
    {
        return null;
    }

    const BindingFlags bindingAttr = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;

    object internalQuery = typeof(DbQuery<T>).GetProperty("InternalQuery", bindingAttr).GetValue(dbQuery);
    return (ObjectQuery)internalQuery.GetType().GetProperty("ObjectQuery", bindingAttr).GetValue(internalQuery);
}
Oct 27, 2014 at 5:52 AM
@davidnemeti - stealing (and thanks for the analysis)

I created a workitem to track fixing this: https://efcache.codeplex.com/workitem/9