Entity Framework - compile safe Includes

by Rudi12. December 2008 06:26

The issue

One of the very nice things of Entity Framework, but mainly a feature of .NET 3.5, is the ability to express a query in a syntax that combines SQL with C#/VB.NET: Linq. Not only does Linq to Entities provide a manner to express queries on the level of ‘conceptual’ entities, it also offers the ability of compile-time validation, which is a feature not to be under-estimated !

Take for instance the following Linq to Entities query:

var query = from p
in context.ProductSet
where p.SalePrice > 1000
select p;

However, Entity Framework lacks compile safety when it comes to include related objects in the query. The Include operation to be used expects a string argument containing a path of properties to include in the query results, as in:

var query = from p
in context.ProductSet
.Include("PriceHistory")
.Include("Supplier.Address")
where p.SalePrice > 1000
select p;

This query, which is supposed to return all products priced over 1000 including their price history, supplier and their address, does compile without compile time check of the included property paths. However, I might have modelled a product as to have several suppliers and renamed the property to it’s plural form ‘Suppliers’.

I found this to be an issue I wanted to solve...

The solution

.NET 3.5 has some very interesting features of which lambda expressions allow for compile time checking of expressions without the requirement to execute them, while extension methods allow us to add or overload methods on classes not under our control.

Combining both techniques, it is possible to offer an alternative Include method which uses a lambda expression as property path. With it, I can rewrite the above query in:

var query = from p
in context.ProductSet
.Include(p => p.PriceHistory)
.Include(p => p.Supplier.Address)
where p.SalePrice > 1000
select p;

Would I refactor my entity model to support multiple suppliers per product, then this query would result in a compile error. I could then rewrite the query as follows:

var query = from p
in context.ProductSet
.Include(p => p.PriceHistory)
.Include(p => p.Suppliers.First().Address)
where p.SalePrice > 1000
select p;

Notice that, as Suppliers has become a collection, I can not simply dereference Address of it. I can only dereference Address on individual items of the suppliers collection. Here I chose to dereference Address on the first item of the collection. In reality, this lambda expression is merely used to produce the property path “Suppliers.Address”, which will eager load the addresses of all suppliers.

The code

The code consists of a ObjectQuery<T> extension method which receives an Expression as argument:

using System;
using System.Collections.Generic;
using System.Data.Objects;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
namespace CodeTuning.Data.Entity
{
/// <summary>
/// Extension methods on ObjectQuery.
/// </summary>
public static class ObjectQueryExtension
{
/// <summary>
/// Specifies the related objects to include in the query results using
/// a lambda expression mentioning the path members.
/// </summary>
/// <returns>A new System.Data.Objects.ObjectQuery<T> with the defined query path.</returns>
public static ObjectQuery<T> Include<T>(this ObjectQuery<T> query, Expression<Func<T, object>> path)
{
// Retrieve member path:
List<PropertyInfo> members = new List<PropertyInfo>();
EntityFrameworkHelper.CollectRelationalMembers(path, members);
// Build string path:
StringBuilder sb = new StringBuilder();
string separator = "";
foreach (MemberInfo member in members)
{
sb.Append(separator);
sb.Append(member.Name);
separator = ".";
}
// Apply Include:
return query.Include(sb.ToString());
}
}
}

The core functionality of 'parsing' the lambda expression is isolated in a separate EntityFrameworkHelper class. I've done this because I reused this parser code for another feature which will be subject of a leter post on this blog:

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
namespace CodeTuning.Data.Entity
{
internal static class EntityFrameworkHelper
{
internal static void CollectRelationalMembers(Expression exp, IList<PropertyInfo> members)
{
if (exp.NodeType == ExpressionType.Lambda)
{
// At root, explore body:
CollectRelationalMembers(((LambdaExpression)exp).Body, members);
}
else if (exp.NodeType == ExpressionType.MemberAccess)
{
MemberExpression mexp = (MemberExpression)exp;
CollectRelationalMembers(mexp.Expression, members);
members.Add((PropertyInfo)mexp.Member);
}
else if (exp.NodeType == ExpressionType.Call)
{
MethodCallExpression cexp = (MethodCallExpression)exp;
if (cexp.Method.IsStatic == false)
throw new InvalidOperationException("Invalid type of expression.");
foreach (var arg in cexp.Arguments)
CollectRelationalMembers(arg, members);
}
else if (exp.NodeType == ExpressionType.Parameter)
{
// Reached the toplevel:
return;
}
else
{
throw new InvalidOperationException("Invalid type of expression.");
}
}
}
}

You can download this code from the following link:
EF_LambdaInclude.zip (1.35 kb)

Tags:

Entity Framework

Comments are closed

About me

Widget Month List not found.

The file '/blog/widgets/Month List/widget.ascx' does not exist.X

Widget Page List not found.

The file '/blog/widgets/Page List/widget.ascx' does not exist.X