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?
We use the include & ThenInclude methods, along with the Projection Query in EF Core to load the related entities. In this tutorial, we look at include method and learn how to load entities from multiple levels and multiple tables.
From EF Core docs... emphasis on the last sentence. You may want to include multiple related entities for one of the entities that is being included. For example, when querying Blogs, you include Postsand then want to include both the Authorand Tagsof the Posts. To do this, you need to specify each include path starting at the root.
The eager loading in EF Core done via the Include & ThenInclude methods. We need to supply the navigational property of the related entity as the argument. The next version of EF Core will also support the filtering & Ordering of the related data. You can apply on multiple tables and at multiple levels etc.
This feature was introduced in EF Core 5.0. When applying Include to load related data, you can add certain enumerable operations to the included collection navigation, which allows for filtering and sorting of the results. Supported operations are: Where, OrderBy, OrderByDescending, ThenBy, ThenByDescending, Skip, and Take.
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!
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);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With