Improving ObjectQuery.Include – Updated

Having spent some time using the sample from my previous post on ObjectQuery.Include, I’ve encountered a bug! It turns out that the code generates the wrong include string for

 context.Customers.Include(c => c.Order.SubInclude(o=>o.OrderDetail))

The fix for this is a small change to the BuildString method to recurse up the MemberExpression if necessary. The updated code is below  - usual disclaimers apply!

     public static class ObjectQueryExtensions
    {
        public static ObjectQuery<TSource> Include<TSource, TPropType>(this ObjectQuery<TSource> source, Expression<Func<TSource, TPropType>> propertySelector)
        {
            string includeString = BuildString(propertySelector);
            return source.Include(includeString);
        }
        private static string BuildString(Expression propertySelector)
        {
            switch (propertySelector.NodeType)
            {
                case ExpressionType.Lambda:
                    LambdaExpression lambdaExpression = (LambdaExpression)propertySelector;
                    return BuildString(lambdaExpression.Body);

                case ExpressionType.Quote:
                    UnaryExpression unaryExpression = (UnaryExpression)propertySelector;
                    return BuildString(unaryExpression.Operand);

                case ExpressionType.MemberAccess:

                    MemberExpression memberExpression = (MemberExpression)propertySelector;
                    MemberInfo propertyInfo = memberExpression.Member;

                    if (memberExpression.Expression is ParameterExpression)
                    {
                        return propertyInfo.Name;
                    }
                    else
                    {
                        // we've got a nested property (e.g. MyType.SomeProperty.SomeNestedProperty)
                        return BuildString(memberExpression.Expression) + "." + propertyInfo.Name;
                    }

                case ExpressionType.Call:
                    MethodCallExpression methodCallExpression = (MethodCallExpression)propertySelector;
                    if (IsSubInclude(methodCallExpression.Method)) // check that it's a SubInclude call
                    {
                        // argument 0 is the expression to which the SubInclude is applied (this could be member access or another SubInclude)
                        // argument 1 is the expression to apply to get the included property
                        // Pass both to BuildString to get the full expression
                        return BuildString(methodCallExpression.Arguments[0]) + "." +
                               BuildString(methodCallExpression.Arguments[1]);
                    }
                    // else drop out and throw
                    break;
            }
            throw new InvalidOperationException("Expression must be a member expression or an SubInclude call: " + propertySelector.ToString());

        }

        private static readonly MethodInfo[] SubIncludeMethods;
        static ObjectQueryExtensions()
        {
            Type type = typeof(ObjectQueryExtensions);
            SubIncludeMethods = type.GetMethods().Where(mi => mi.Name == "SubInclude").ToArray();
        }
        private static bool IsSubInclude(MethodInfo methodInfo)
        {
            if (methodInfo.IsGenericMethod)
            {
                if (!methodInfo.IsGenericMethodDefinition)
                {
                    methodInfo = methodInfo.GetGenericMethodDefinition();
                }
            }
            return SubIncludeMethods.Contains(methodInfo);
        }

        public static TPropType SubInclude<TSource, TPropType>(this EntityCollection<TSource> source, Expression<Func<TSource, TPropType>> propertySelector)
            where TSource : class, IEntityWithRelationships
            where TPropType : class
        {
            throw new InvalidOperationException("This method is only intended for use with ObjectQueryExtensions.Include to generate expressions trees"); // no actually using this - just want the expression!
        }
        public static TPropType SubInclude<TSource, TPropType>(this TSource source, Expression<Func<TSource, TPropType>> propertySelector)
            where TSource : class, IEntityWithRelationships
            where TPropType : class
        {
            throw new InvalidOperationException("This method is only intended for use with ObjectQueryExtensions.Include to generate expressions trees"); // no actually using this - just want the expression!
        }
    }

 

UPDATE: Alex James has a great post on Eager Loading Strategies that I'd recommend reading.

Comments

  • Anonymous
    April 24, 2009
    PingBack from http://www.anith.com/?p=32024

  • Anonymous
    June 13, 2010
    The formatting of this blog makes it impossible to read the code, it clips the text to the right.

  • Anonymous
    June 13, 2010
    Hi Thomas - I agree! blogs.msdn.com got a major upgrade recently and I've not yet had chance to go through and try to fix the formatting issues that it introduced. The code does seem to copy correctly if that's any help

  • Anonymous
    June 13, 2010
    In EF4 you can include for EntityCollection and then to digg deeper on he type of the collection. Is your solution support it?

  • Anonymous
    June 13, 2010
    Hi Noam, The example at the top of the post:    context.Customers.Include(c => c.Orders.SubInclude(o=>o.OrderDetails)) corresponds to    context.Customers.Include("Orders.OrderDetails") Is that what you are referring to? --Stuart

  • Anonymous
    May 02, 2014
    Does this work in VS 2012 with Framework 4.0?

  • Anonymous
    May 12, 2014
    @Mark - I've definitely used this approach with EF 4, EF 5 and EF 6. If you're working with the DbQuery API rather than ObjectQuery API then there are a few tweaks that you need to make to the code, but the approach still works