Entity Framework .Include() with compile time checking?
Asked Answered
P

5

9

Consider the following code, which is calling against an EF generated data context:

var context = new DataContext();
var employees = context.Employees.Include("Department");

If I change the name of the Department relationship then this code is going to start throwing a runtime error. So is there any way to call the .Include() method in a safe manner, so I get compile time checking for all the relationships being referenced?

Prioress answered 27/5, 2010 at 12:37 Comment(0)
P
8

Taking moi_meme's idea a step further, my colleague developed the following solution that works in all cases. He introduced a new method caled Includes() for dealing with one-to-many and many-to-many relationships. It allows you to write this:

context.Customer
    .Include("Address")
    .Include("Orders")
    .Include("Orders.OrderLines")

as this:

context.Customer
    .Include(c => c.Address)
    .Includes(c => c.Include(customer => customer.Orders)
                    .Include(order => order.OrderLines))

All credit goes to https://stackoverflow.com/users/70427/bojan-resnik, so go give him some love if you like the solution.

public static class ObjectQueryExtensions
{
    public static ObjectQuery<T> Includes<T>(this ObjectQuery<T> query, Action<IncludeObjectQuery<T, T>> action)
    {
        var sb = new StringBuilder();
        var queryBuilder = new IncludeObjectQuery<T, T>(query, sb);
        action(queryBuilder);
        return queryBuilder.Query;
    }

    public static ObjectQuery<TEntity> Include<TEntity, TProperty>(this ObjectQuery<TEntity> query, Expression<Func<TEntity, TProperty>> expression)
    {
        var sb = new StringBuilder();
        return IncludeAllLevels(expression, sb, query);
    }

    static ObjectQuery<TQuery> IncludeAllLevels<TEntity, TProperty, TQuery>(Expression<Func<TEntity, TProperty>> expression, StringBuilder sb, ObjectQuery<TQuery> query)
    {
        foreach (var name in expression.GetPropertyLevels())
        {
            sb.Append(name);
            query = query.Include(sb.ToString());
            Debug.WriteLine(string.Format("Include(\"{0}\")", sb));
            sb.Append('.');
        }
        return query;
    }

    static IEnumerable<string> GetPropertyLevels<TClass, TProperty>(this Expression<Func<TClass, TProperty>> expression)
    {
        var namesInReverse = new List<string>();

        var unaryExpression = expression as UnaryExpression;
        var body = unaryExpression != null ? unaryExpression.Operand : expression.Body;

        while (body != null)
        {
            var memberExpression = body as MemberExpression;
            if (memberExpression == null)
                break;

            namesInReverse.Add(memberExpression.Member.Name);
            body = memberExpression.Expression;
        }

        namesInReverse.Reverse();
        return namesInReverse;
    }

    public class IncludeObjectQuery<TQuery, T>
    {
        readonly StringBuilder _pathBuilder;
        public ObjectQuery<TQuery> Query { get; private set; }

        public IncludeObjectQuery(ObjectQuery<TQuery> query, StringBuilder builder)
        {
            _pathBuilder = builder;
            Query = query;
        }

        public IncludeObjectQuery<TQuery, U> Include<U>(Expression<Func<T, U>> expression)
        {
            Query = ObjectQueryExtensions.IncludeAllLevels(expression, _pathBuilder, Query);
            return new IncludeObjectQuery<TQuery, U>(Query, _pathBuilder);
        }

        public IncludeObjectQuery<TQuery, U> Include<U>(Expression<Func<T, EntityCollection<U>>> expression) where U : class
        {
            Query = ObjectQueryExtensions.IncludeAllLevels(expression, _pathBuilder, Query);
            return new IncludeObjectQuery<TQuery, U>(Query, _pathBuilder);
        }
    }
}
Prioress answered 2/6, 2011 at 5:55 Comment(1)
Very impressive, i guess i'm gonna have to try that :) cheers!!Imaimage
E
8

I have used the following with Entity Framework 5. The key is to include System.Data.Entity

using System.Data.Entity;

context.Customer
    .Include(c => c.Address)
Enterpriser answered 8/1, 2014 at 20:7 Comment(1)
Thanks, the Include is why other solutions weren't working! This is by far the simplest solution.Trichloroethylene
I
6

I did a little extension to ObjectQuery which goes like this

public static ObjectQuery<TEntity> Include<TEntity, TProperty>(this ObjectQuery<TEntity> query, Expression<Func<TEntity, TProperty>> expression) where TEntity : class
{
    string name = expression.GetPropertyName();
    return query.Include(name);
}

which also requires

public static class ExpressionExtensions
{
    public static string GetPropertyName<TObject, TProperty>(this Expression<Func<TObject, TProperty>> expression) where TObject : class
    {
        if (expression.Body.NodeType == ExpressionType.Call)
        {
            MethodCallExpression methodCallExpression = (MethodCallExpression)expression.Body;
            string name = ExpressionExtensions.GetPropertyName(methodCallExpression);
            return name.Substring(expression.Parameters[0].Name.Length + 1);
        }
        return expression.Body.ToString().Substring(expression.Parameters[0].Name.Length + 1);
    }

    private static string GetPropertyName(MethodCallExpression expression)
    {
        MethodCallExpression methodCallExpression = expression.Object as MethodCallExpression;
        if (methodCallExpression != null)
        {
            return GetPropertyName(methodCallExpression);
        }
        return expression.Object.ToString();
    }
}

with that you can do

var context = new DataContext();      
var employees = context.Employees.Include(e => e.Department);

which is going to be check at compile time. If i remember correctly, this methods doesn't work for many-to-many relationship but it works for stuff like

var item = context.Employees.Include(e => e.Department.Manager);

Good luck to you

Imaimage answered 27/5, 2010 at 12:49 Comment(7)
All it is doing is taking the name of the property and putting it to string so your Entity must have the same name as that property for his to work, so it might work for many-to-manyImaimage
Correct me if im wrong, but this won't work for double-nested nav's (e.g Orders.Details.Products).Cilo
@Cilo it won't work when it's an EntityCollection... you cannot refer properties of an entity collection.Imaimage
@Imaimage - not sure what you mean, but if im using pure POCO's, and if i have three entities, sometimes i want to eager load the second and third entities when i retrieve the first. Therefore the expression would need to be Include(x => x.Order.Details.Products) (Details/Products is ICollection<T>). I don't think this would create the . required for the include string.Cilo
@Cilo - It doesn't work for collection since simply writing Include(x => x.Order.Details.Products) will generate an compile time error...Imaimage
why would that generate an error? Products is a property. If it can't work for collections then there isn't much benefit in this extension.Cilo
I know it's been a while, but see my answer for a full and general solution.Prioress
P
2
var context = new DataContext();
var employees = context.Employees.Include(context.Department.EntitySet.Name);
Ppm answered 10/4, 2012 at 13:25 Comment(0)
S
1

In case you're still using versions prior to Entity Framework 5, the good news is as of C# 6, you can now use nameof to retrieve name of any class/object.

So now you may do

var context = new DataContext();
var employees = context.Employees.Include(nameof(Employees.Department));

If you're using EF > 5, then Xavier's answer is better

Sledgehammer answered 20/11, 2015 at 19:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.