38

I have an extension method that lets you generically include data in EF:

public static IQueryable<T> IncludeMultiple<T>(this IQueryable<T> query, params Expression<Func<T, object>>[] includes)
    where T : class
{
    if (includes != null)
    {
        query = includes.Aggregate(query, (current, include) => current.Include(include));
    }
    return query;
}

This allows me to have methods in my repository like this:

public Patient GetById(int id, params Expression<Func<Patient, object>>[] includes)
{
    return context.Patients
        .IncludeMultiple(includes)
        .FirstOrDefault(x => x.PatientId == id);
}

I believe the extension method worked before EF Core, but now including "children" is done like this:

var blogs = context.Blogs
    .Include(blog => blog.Posts)
        .ThenInclude(post => post.Author);

Is there a way to alter my generic extension method to support EF Core's new ThenInclude() practice?

mellis481
  • 4,332
  • 12
  • 71
  • 118
  • Just do `current.Include(include)` for the first expression in your array and ` current.ThenInclude(include)` for the rest? – Evk Mar 20 '17 at 13:09
  • @Evk You're passing a Linq expression into this extension method so the input would be something like this: `x => x.Posts.Select(p => p.Author)`. – mellis481 Mar 20 '17 at 13:11
  • I see what you mean. Did you test that old syntax does not work in EF Core? I heard that it does, but did not test myself. – Evk Mar 20 '17 at 13:22
  • @Evk It does not work the way I have it set. I get the following error (altered using Blog-Post example): `'The property expression 'x => {from Post c in [x].Posts select [c].Author}' is not valid. The expression should represent a property access: 't => t.MyProperty'.` – mellis481 Mar 20 '17 at 14:09
  • It will be not easy to do.. You have to use the name syntax for nested includes (with `Select`)? So I mean you want to reuse the same include syntax between EF6 and EF core? – Evk Mar 20 '17 at 16:55
  • @Evk I don't care if I can use the same syntax. Ultimately, I just want to way to create a extension method where I can pass in all includes to a single generic method. – mellis481 Mar 20 '17 at 18:14
  • Well, you can easily do that by passing `params string[] includes` and use the string overload of `Include` method, but you might not like it because of the "magic string" usage. – Ivan Stoev Mar 20 '17 at 18:49
  • Yes, was going to mention that there is overload that accepts string. Is that fine for you? – Evk Mar 20 '17 at 19:06
  • 1
    Another way is to parse the passed expressions and build string include path like the EF6 code - [source](https://github.com/aspnet/EntityFramework6/blob/master/src/EntityFramework/Internal/DbHelpers.cs#L260). – Ivan Stoev Mar 20 '17 at 19:15
  • 1
    @IvanStoev it's funny that I wrote almost exactly the same method as provided at your link. Was stupid for me to not just grab it from source indeed... – Evk Mar 20 '17 at 20:03
  • @IvanStoev: Where can I find `RemoveConvert()`? – mellis481 Mar 20 '17 at 20:13
  • [here](https://github.com/aspnet/EntityFramework6/blob/master/src/EntityFramework/Utilities/ExpressionExtensions.cs#L186) – Ivan Stoev Mar 20 '17 at 23:14
  • @Evk I'm pretty sure you wrote it better though :) – Ivan Stoev Mar 20 '17 at 23:28
  • 1
    So nothing from the comments helped (includes as strings or EF6 source code for conversion)? – Evk Mar 23 '17 at 20:08
  • @Evk I wasn't able to figure it out in the limited time I have. I thought I'd reward someone if they were willing to take a little time to share some code. – mellis481 Mar 24 '17 at 12:56
  • It's unclear what you are asking though. Du you want (A) to keep your repository and extension method signatures (hence calling them with the old syntax) and modify the extension method implementation to convert it to EF Core includes, or (2) to change the repository method signature (hence the call syntax)? – Ivan Stoev Mar 24 '17 at 19:25
  • @IvanStoev I'd prefer to keep the `param` type of my extension method, but ultimately I just want a method that will allow me to generically include multiple potentially-nested properties. – mellis481 Mar 24 '17 at 20:23
  • See, the dilemma is, you either keep the old signatures and call them with old `Select` style, or you don't need the `params` and custom extension method because EF Core `Include` is restartable, i.e. it doesn't need multiple include syntax because they are all chained. e.g. `db.Root.Incude(r => r.Col1).ThenInclude(c1 => c1.Prop1).Include(r => r.Col2).ThenInclude(c2 => c2.Prop2).Include(r => r.Prop3)` etc. So this is the new syntax and is simply achieved by custom chaining `IQueryable` methods, thus cannot be expressed with `params Expression>[]`. – Ivan Stoev Mar 24 '17 at 20:31
  • Do `Include` and `ThenInclude` return a different types of object than a raw vanilla `IQueryable` (under the covers)? Similar to how `OrderBy` and `ThenBy` return an `IOrderedEnumerable`, so you know that if your input is an `IEnumerable` it takes an `OrderBy`, but if it is an `IOrderedEnumerable` you need a `ThenBy` to preserve the existing ordering. – ErikE Mar 27 '17 at 17:23

9 Answers9

19

As said in comments by other, you can take EF6 code to parse your expressions and apply the relevant Include/ThenInclude calls. It does not look that hard after all, but as this was not my idea, I would rather not put an answer with the code for it.

You may instead change your pattern for exposing some interface allowing you to specify your includes from the caller without letting it accessing the underlying queryable.

This would result in something like:

using YourProject.ExtensionNamespace;

// ...

patientRepository.GetById(0, ip => ip
    .Include(p => p.Addresses)
    .ThenInclude(a=> a.Country));

The using on namespace must match the namespace name containing the extension methods defined in the last code block.

GetById would be now:

public static Patient GetById(int id,
    Func<IIncludable<Patient>, IIncludable> includes)
{
    return context.Patients
        .IncludeMultiple(includes)
        .FirstOrDefault(x => x.EndDayID == id);
}

The extension method IncludeMultiple:

public static IQueryable<T> IncludeMultiple<T>(this IQueryable<T> query,
    Func<IIncludable<T>, IIncludable> includes)
    where T : class
{
    if (includes == null)
        return query;

    var includable = (Includable<T>)includes(new Includable<T>(query));
    return includable.Input;
}

Includable classes & interfaces, which are simple "placeholders" on which additional extensions methods will do the work of mimicking EF Include and ThenInclude methods:

public interface IIncludable { }

public interface IIncludable<out TEntity> : IIncludable { }

public interface IIncludable<out TEntity, out TProperty> : IIncludable<TEntity> { }

internal class Includable<TEntity> : IIncludable<TEntity> where TEntity : class
{
    internal IQueryable<TEntity> Input { get; }

    internal Includable(IQueryable<TEntity> queryable)
    {
        // C# 7 syntax, just rewrite it "old style" if you do not have Visual Studio 2017
        Input = queryable ?? throw new ArgumentNullException(nameof(queryable));
    }
}

internal class Includable<TEntity, TProperty> :
    Includable<TEntity>, IIncludable<TEntity, TProperty>
    where TEntity : class
{
    internal IIncludableQueryable<TEntity, TProperty> IncludableInput { get; }

    internal Includable(IIncludableQueryable<TEntity, TProperty> queryable) :
        base(queryable)
    {
        IncludableInput = queryable;
    }
}

IIncludable extension methods:

using Microsoft.EntityFrameworkCore;

// others using ommitted

namespace YourProject.ExtensionNamespace
{
    public static class IncludableExtensions
    {
        public static IIncludable<TEntity, TProperty> Include<TEntity, TProperty>(
            this IIncludable<TEntity> includes,
            Expression<Func<TEntity, TProperty>> propertySelector)
            where TEntity : class
        {
            var result = ((Includable<TEntity>)includes).Input
                .Include(propertySelector);
            return new Includable<TEntity, TProperty>(result);
        }

        public static IIncludable<TEntity, TOtherProperty>
            ThenInclude<TEntity, TOtherProperty, TProperty>(
                this IIncludable<TEntity, TProperty> includes,
                Expression<Func<TProperty, TOtherProperty>> propertySelector)
            where TEntity : class
        {
            var result = ((Includable<TEntity, TProperty>)includes)
                .IncludableInput.ThenInclude(propertySelector);
            return new Includable<TEntity, TOtherProperty>(result);
        }

        public static IIncludable<TEntity, TOtherProperty>
            ThenInclude<TEntity, TOtherProperty, TProperty>(
                this IIncludable<TEntity, IEnumerable<TProperty>> includes,
                Expression<Func<TProperty, TOtherProperty>> propertySelector)
            where TEntity : class
        {
            var result = ((Includable<TEntity, IEnumerable<TProperty>>)includes)
                .IncludableInput.ThenInclude(propertySelector);
            return new Includable<TEntity, TOtherProperty>(result);
        }
    }
}

IIncludable<TEntity, TProperty> is almost like IIncludableQueryable<TEntity, TProperty> from EF, but it does not extend IQueryable and does not allow reshaping the query.

Of course if the caller is in the same assembly, it can still cast the IIncludable to Includable and start fiddling with the queryable. But well, if someone wants to get it wrong, there is no way we would prevent him doing so (reflection allows anything). What does matter is the exposed contract.

Now if you do not care about exposing IQueryable to the caller (which I doubt), obviously just change your params argument for a Func<Queryable<T>, Queryable<T>> addIncludes argument, and avoid coding all those things above.

And the best for the end: I have not tested this, I do not use Entity Framework currently!

Frédéric
  • 9,364
  • 3
  • 62
  • 112
  • Did you fully test this? I can't get it to work. I think there are a couple typos... – mellis481 Mar 30 '17 at 00:24
  • It compiles (with a local copy of IIncludableQueryable from EF.Core), but it is not tested. If you have compilation in the IIncludable extension methods, have you the required `using` from EF for having EF include extensions methods? – Frédéric Mar 30 '17 at 01:01
  • Gotcha. I've implemented all your code, but I can't get the call to the repository with the Include lambda to work. Intellisense is providing no reference to Include... – mellis481 Mar 30 '17 at 19:00
  • You mean, you were missing `using Microsoft.EntityFrameworkCore;` in the file defining my above extension methods, and now it works? – Frédéric Mar 30 '17 at 20:55
  • Or you are missing in the file containing the code calling your repository a `using` on the namespace containing the class defining my above extension methods? – Frédéric Mar 30 '17 at 20:56
  • I have added explicit namespace and using requirement in my answer. I may even post the first version I had elaborated, which is without interfaces and without extension methods apart your `IncludeMultiple`. And a simple test to check if you have not a `using` or assembly reference issue: try calling the extension methods directly as static method of their containing class, like `ip => IncludableExtensions.Include(ip, p => p.Addresses)`. If you cannot, fix your using and/or project references and/or class and method visibility. – Frédéric Mar 30 '17 at 21:10
11

For posterity, another less eloquent, but simpler solution that makes use of the Include() overload that uses navigationPropertyPath:

public static class BlogIncludes
{
    public const string Posts = "Posts";
    public const string Author = "Posts.Author";
}

internal static class DataAccessExtensions
{
    internal static IQueryable<T> IncludeMultiple<T>(this IQueryable<T> query, 
        params string[] includes) where T : class
    {
        if (includes != null)
        {
            query = includes.Aggregate(query, (current, include) => current.Include(include));
        }
        return query;
    }
}

public Blog GetById(int ID, params string[] includes)
{
    var blog = context.Blogs
        .Where(x => x.BlogId == id)
        .IncludeMultiple(includes)
        .FirstOrDefault();
    return blog;
}

And the repository call is:

var blog = blogRepository.GetById(id, BlogIncludes.Posts, BlogIncludes.Author);
mellis481
  • 4,332
  • 12
  • 71
  • 118
2

You can do something like this:

public Patient GetById(int id, Func<IQueryable<Patient>, IIncludableQueryable<Patient, object>> includes = null)
        {
            IQueryable<Patient> queryable = context.Patients;

            if (includes != null)
            {
                queryable = includes(queryable);
            }

            return  queryable.FirstOrDefault(x => x.PatientId == id);
        }

var patient = GetById(1, includes: source => source.Include(x => x.Relationship1).ThenInclude(x => x.Relationship2));
2

I made this method to do the dynamic includes. This way the "Select" command can be used in lambda to include just as it was in the past.

The call works like this:

repository.IncludeQuery(query, a => a.First.Second.Select(b => b.Third), a => a.Fourth);

private IQueryable<TCall> IncludeQuery<TCall>(
    params Expression<Func<TCall, object>>[] includeProperties) where TCall : class
{
    IQueryable<TCall> query;

    query = context.Set<TCall>();

    foreach (var property in includeProperties)
    {
        if (!(property.Body is MethodCallExpression))
            query = query.Include(property);
        else
        {
            var expression = property.Body as MethodCallExpression;

            var include = GenerateInclude(expression);

            query = query.Include(include);
        }
    } 

    return query;
}

private string GenerateInclude(MethodCallExpression expression)
{
    var result = default(string);

    foreach (var argument in expression.Arguments)
    {
        if (argument is MethodCallExpression)
            result += GenerateInclude(argument as MethodCallExpression) + ".";
        else if (argument is MemberExpression)
            result += ((MemberExpression)argument).Member.Name + ".";
        else if (argument is LambdaExpression)
            result += ((MemberExpression)(argument as LambdaExpression).Body).Member.Name + ".";
    }

    return result.TrimEnd('.');
} 
William Magno
  • 502
  • 5
  • 6
1

Ofcourse there is,

you could traverse the Expression tree of original params, and any nested includes, add them as

 .Include(entity => entity.NavigationProperty)
 .ThenInclude(navigationProperty.NestedNavigationProperty)

But its not trivial, but definitely very doable, please share if you do, as it can defintiely be reused!

Michal Ciechan
  • 13,492
  • 11
  • 76
  • 118
1
public Task<List<TEntity>> GetAll()
    {
        var query = _Db.Set<TEntity>().AsQueryable();
        foreach (var property in _Db.Model.FindEntityType(typeof(TEntity)).GetNavigations())
            query = query.Include(property.Name);
        return query.ToListAsync();

    }
1

I adhere the simpler solution that makes use of the Include() overload that uses string navigationPropertyPath. The simplest that I can write is this extension method below.

using Microsoft.EntityFrameworkCore;
using System.Linq;

namespace MGame.Data.Helpers
{
    public static class IncludeBuilder
    {
        public static IQueryable<TSource> Include<TSource>(this IQueryable<TSource> queryable, params string[] navigations) where TSource : class
        {
            if (navigations == null || navigations.Length == 0) return queryable;

            return navigations.Aggregate(queryable, EntityFrameworkQueryableExtensions.Include);  // EntityFrameworkQueryableExtensions.Include method requires the constraint where TSource : class
        }
    }
}
alhpe
  • 1,424
  • 18
  • 24
1

I handle it with like that;

I have Article entity. It includes ArticleCategory entity. And also ArticleCategory entity includes Category entity.

So: Article -> ArticleCategory -> Category

In My Generic Repository;

public virtual IQueryable<T> GetIncluded(params Func<IQueryable<T>, IIncludableQueryable<T, object>>[] include)
{
     IQueryable<T> query = Entities; // <- this equals = protected virtual DbSet<T> Entities => _entities ?? (_entities = _context.Set<T>());
     if (include is not null)
     {
          foreach (var i in include)
          {
              query = i(query);
          }
     }
     return query;
}

And I can use it like that;

var query = _articleReadRepository.GetIncluded(
               i => i.Include(s => s.ArticleCategories).ThenInclude(s => s.Category),
               i => i.Include(s => s.User)
            );
Abdulkadir KG
  • 83
  • 1
  • 8
0
    public TEntity GetByIdLoadFull(string id, List<string> navigatonProoperties)
    {
        if (id.isNullOrEmpty())
        {
            return null;
        }

        IQueryable<TEntity> query = dbSet;

        if (navigationProperties != null)
        {
            foreach (var navigationProperty in navigationProperties)
            {
                query = query.Include(navigationProperty.Name);
            }
        }

        return query.SingleOrDefault(x => x.Id == id);
    }

Here is a much simpler solution, idea is to cast the dbset to iqueryable and then recursively include properties

jayasurya_j
  • 1,519
  • 1
  • 15
  • 22