Say goodbye to the hardcoded ObjectQuery(T).Include calls

06/08/2010

When you want to eagerly-load navigation-properties with ADO.NET Entity-Framework, you use the ObjectQuery(T).Include method, which takes one argument ‘path’ typed String.

I often find myself changing my database and model by adding / removing / renaming properties or other fields,
and I don’t realize that an exception thrown in one lonesome window is because of thes changes,
all because only hard-coded strings are supported!

So I decided to make my own Include implementation, and thanks to Extension Methods, it’s hell of a lot easier to use.

So, in order to enjoy the ‘easycoded’ Includes, just copy this module onto your solution and your ready to go:

 


Imports System.Reflection
Imports System.Data.Objects
Imports System.Linq.Expressions
Imports System.Runtime.CompilerServices
<HideModuleName()>
Public Module ObjectQueryExtensions

  ''' <summary>
  ''' Specifies the related objects to include in the query results.	
  ''' </summary>
  ''' <typeparam name="TSource">The entity type of the query.</typeparam>
  ''' <typeparam name="TResult">The type of the value returned by selector.</typeparam>
  ''' <param name="query"><see cref="System.Data.Objects.ObjectQuery(Of T)"/>.</param>
  ''' <param name="path">The include path.</param>

  ''' <returns>A new <see cref="System.Data.Objects.ObjectQuery(Of T)"/> with the defined query path.</returns>
  ''' <exception cref="ArgumentNullException">query is null.</exception>
  ''' <exception cref="ArgumentException">
  ''' Invalid or not supported selector tree, or not supported methods are used (see remarks).
  ''' </exception>
  ''' <remarks>
  ''' Use <see cref="System.Linq.Enumerable.Single(Of T)"/> extension method
  ''' to singularize a navigation-properties and to be able to access its child properties and materialize them.	
  ''' </remarks>
  <Extension()>
  Public Function Include(Of TSourceTResult)(ByVal query As ObjectQuery(Of TSource),
      ByVal path As Expression(Of Func(Of TSourceTResult))) As ObjectQuery(Of TSource)
    If query Is Nothing Then Throw New ArgumentNullException("query")

    Dim properties As New List(Of String)
    Dim add = Sub([property] As Stringproperties.Insert(0, [property])
    Dim expression = path.Body
    Do
      Select Case expression.NodeType
        Case ExpressionType.MemberAccess
          Dim member = DirectCast(expressionMemberExpression)
          If member.Member.MemberType <> MemberTypes.Property Then _
            Throw New ArgumentException("The selected member was not a property.""selector")

          add(member.Member.Name)
          expression = member.Expression
        Case ExpressionType.Call
          Dim method = DirectCast(expressionMethodCallExpression)

          If method.Method.Name <> SingleMethodName OrElse method.Method.DeclaringType <> EnumerableType Then _
           Throw New ArgumentException(
             String.Format("Method '{0}' is not supported, only method '{1}' is supported to singularize navigation properties.",
               String.Join(Type.Delimitermethod.Method.DeclaringType.FullNamemethod.Method.Name),
               String.Join(Type.DelimiterEnumerableType.FullNameSingleMethodName)),
             "selector")

          Dim argument = DirectCast(method.Arguments.SingleUnaryExpression)
          expression = argument.Operand
        Case Else
          Throw New ArgumentException("The property selector expression has an incorrect format.",
                                      "selector",
                                      New FormatException)
      End Select
    Loop Until expression.NodeType = ExpressionType.Parameter

    Return query.Include(String.Join(Type.Delimiterproperties))
  End Function
  Private ReadOnly EnumerableType As Type = GetType(System.Linq.Enumerable)
  Private Const SingleMethodName As String = "Single"
End Module

Then it can be easily used as follows:

So instead of writing:

	context.Cars.Include("Finish.Style.Model.Vendor.Contact.Addresses")

Just write:

	context.Cars.Include(Function(cc.Finish.Style.Model.Vendor.Contact.Addresses)

For navigation properties of many-2-many, use the Enumerable.Single(TSource)) extension method to be able to access its child properties:

	context.Cars.Include("Finish.Style.Model.Vendor.Contact.Addresses.State")

Goes into:

	context.Cars.Include(Function(cc.Finish.Style.Model.Vendor.Contact.Addresses.Single.State
I translated the above to C#, and when testing I found something very odd, when it fall in the ExpressionType.Call case, 
the method argument is the direct MemberExpression, rather than an UnaryExpression which wraps the MemberExpression as its operand, very weird,
please accept:
using System;
using System.Collections.Generic;
using System.Data.Objects;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
namespace ConsoleApplication1
{
  public static class ObjectQueryExtensions
  {
    static void Main()
    {
      using (var context = new Entities())
      {
        var x = context.Cars.Include(c => c.Finish.Style.Model.Vendor.Contact.Addresses);
        var y = x.ToArray();
      }
    }

    public static ObjectQuery<TSource> Include<TSourceTResult>(
      this ObjectQuery<TSource> query,
      Expression<Func<TSource, TResult>> path)
    {
      if (query == nullthrow new ArgumentException("query");

      var properties = new List<string>();
      Action<stringadd = (str) => properties.Insert(0, str);
      var expression = path.Body;

      do
      {
        switch (expression.NodeType)
        {
          case ExpressionType.MemberAccess:
            var member = (MemberExpression)expression;
            if (member.Member.MemberType != MemberTypes.Property)
              throw new ArgumentException("The selected member must be a property.""selector");

            add(member.Member.Name);
            expression = member.Expression;
            break;
          case ExpressionType.Call:
            var method = (MethodCallExpression)expression;

            if (method.Method.Name != SingleMethodName || method.Method.DeclaringType != EnumerableType)
              throw new ArgumentException(
                string.Format("Method '{0}' is not supported, only method '{1}' is supported to singularize navigation properties.",
                string.Join(Type.Delimiter.ToString(), method.Method.DeclaringType.FullNamemethod.Method.Name),
                string.Join(Type.Delimiter.ToString(), EnumerableType.FullNameSingleMethodName)),
                "selector");

            expression = (MemberExpression)method.Arguments.Single();
            break;
          default:
            throw new ArgumentException("The property selector expression has an incorrect format.", 
              "selector", 
              new FormatException());
        }

      } while (expression.NodeType != ExpressionType.Parameter);

      return query.Include(string.Join(Type.Delimiter.ToString(), properties));
    }
    private static readonly Type EnumerableType = typeof(Enumerable);
    private const string SingleMethodName = "Single";
  }
}

 

Using the strongly-typed linq-expression based Include function, you gain two advantages,

firstly, as said, you can use known strings, rather than hard-coded strings,
and then if you change a property/class-name, or you remove a column (property), it will throw a compile-error for the missing path

And by the way, what I do in these case in order to avoid fixing all the errors is, before I rename the property in the entity editor,
I open the generated code (Model.Designer.vb) and change the property name on the code to the desired name,
then you’ll see a smart-tag (see snapshot), which when you click on it, it will change all the references of it.
You can then go ahead and reaname the property on the designer it will match all the references you just rename to the new one:

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

8 comments

  1. Thomas Sczyrba26/01/2011 ב 18:40

    Hello again,
    one more Question:
    Why you use this:

    var properties = new List();
    Action
    add = (str) => properties.Insert(0, str);
    add(member.Member.Name);

    instead of this:
    var properties =new properties ();
    properties.Push(member.Member.Name);

    Greetz from Germany,
    Thomas Sczyrba

    Reply
  2. Shimmy31/03/2011 ב 05:25

    @Thomas, what do you mean ‘properties’? why would u wanna use a Stack? a List does actually the same. a Stack/Queue is for LIFO/FIFO operations where you want to maintain last object etc., that’s not what we need here. I would think of Stack as a higher performance consumer (again, not based on anything, I never tested this) than List.

    Reply
  3. chandra21/11/2011 ב 08:49

    how can we use objectQuery instead of objectQuery when we have the objectquery object of type objectquery.
    I elaborate the problem.
    i have one method which return the object of type ObjectQuery but i m holding this object in objectQuery type variable now i want to include tables of this object but he is giving me the error
    “Late bound operations cannot be performed on types or methods for which ContainsGenericParameters is true.”
    can u tell me reason for this with solution

    Reply
  4. Shimmy21/11/2011 ב 09:24

    @Chandra, you could use:

    ObjectQuery youCollection = GetMyObjectQuery();
    var cast = (ObjectQuery)yourCollection;
    var include = cast.Include(…);

    Note that if you use EF 4.1 and up, there are stongly-typed Include methods that do just what the above post is meant to, and provide further functionality (i.e. nested Where queries).

    I would recommend you on checking out this:
    http://msdn.microsoft.com/en-us/library/gg671236(VS.103).aspx

    Reply
  5. chandra21/11/2011 ב 10:31

    but i don’t know the type of table which has been included to the object query,it will be decided at run time and it will come using the string value then how could i add the tables in the objectquery<>

    Reply
  6. Shimmy21/11/2011 ב 12:22

    I’m sorry but i think you’d better be off using the hardcoded version of the Include that comes in the system.
    The reason is, because the entire Expression> idea is based on compile-time inference.

    I’m sure It’s possible to somehow invoke this function with a reflection loop that will walk thru all the options using MethodInfo.MakeGenericMethod etc. as well as building the expression trees at runtime (which is possible :P ), but this will consume too much performance and development effort.

    So after you seriously make your performance considerations, you can add an extension like the following, I never tested it so I’m curious to know if it’s gonna work (paste the following method into the ObjectQueryExtensions class):

    public static ObjectQuery Include(this ObjectQuery query, Type collectionType, string path)
    {
    if (query == null) throw new ArgumentNullException(“query”);
    var type = typeof(ObjectQuery<>);
    type = type.MakeGenericType(collectionType);
    var method = type.GetMethod(“Include”);
    return (ObjectQuery)method.Invoke(query, new string[] { path });
    }

    Hope this helps,
    Shimmy

    Reply
  7. chandra21/11/2011 ב 14:12

    its not helpful me its calling the include method but its not adding the table in the objectquery,

    Reply
  8. Shimmy21/11/2011 ב 15:02

    Did you use the result or you just ran the function, i.e. var x = query.Include()?

    It’s very weird, because if the function on the class has been invoked successfully. I don’t know what to say.
    Try to figure out what’s going on under the scenes by disabling “Just My Code” (and in case the source for EF is available).

    Reply