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">A <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 TSource, TResult)(ByVal query As ObjectQuery(Of TSource),
ByVal path As Expression(Of Func(Of TSource, TResult))) 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 String) properties.Insert(0, [property])
Dim expression = path.Body
Do
Select Case expression.NodeType
Case ExpressionType.MemberAccess
Dim member = DirectCast(expression, MemberExpression)
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(expression, MethodCallExpression)
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.Delimiter, method.Method.DeclaringType.FullName, method.Method.Name),
String.Join(Type.Delimiter, EnumerableType.FullName, SingleMethodName)),
"selector")
Dim argument = DirectCast(method.Arguments.Single, UnaryExpression)
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.Delimiter, properties))
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(c) c.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(c) c.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<TSource, TResult>(
this ObjectQuery<TSource> query,
Expression<Func<TSource, TResult>> path)
{
if (query == null) throw new ArgumentException("query");
var properties = new List<string>();
Action<string> add = (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.FullName, method.Method.Name),
string.Join(Type.Delimiter.ToString(), EnumerableType.FullName, SingleMethodName)),
"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:
