web-dev-qa-db-fra.com

Plusieurs inclus () dans EF Core

J'ai une méthode d'extension qui vous permet d'inclure génériquement des données dans 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;
}

Cela me permet d'avoir des méthodes dans mon référentiel comme ceci:

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

Je crois que la méthode d'extension fonctionnait avant EF Core, mais maintenant inclure les "enfants" se fait comme ceci:

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

Existe-t-il un moyen de modifier ma méthode d'extension générique pour prendre en charge la nouvelle pratique ThenInclude() d'EF Core?

17
im1dermike

Comme indiqué dans commentaires par d'autres, vous pouvez prendre code EF6 pour analyser vos expressions et appliquer les appels Include/ThenInclude pertinents. Cela ne semble pas si difficile après tout, mais comme ce n'était pas mon idée, je préfère ne pas y répondre avec le code.

Vous pouvez à la place changer votre modèle pour exposer une interface vous permettant de spécifier vos inclusions de l'appelant sans lui permettre d'accéder à la requête sous-jacente.

Cela se traduirait par quelque chose comme:

using YourProject.ExtensionNamespace;

// ...

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

using sur l'espace de noms doit correspondre au nom de l'espace de noms contenant les méthodes d'extension définies dans le dernier bloc de code.

GetById serait maintenant:

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

La méthode d'extension 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 et interfaces, qui sont de simples "espaces réservés" sur lesquels des méthodes d'extensions supplémentaires feront le travail d'imitation des méthodes EF Include et ThenInclude:

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;
    }
}

Méthodes d'extension IIncludable:

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> est presque comme IIncludableQueryable<TEntity, TProperty> d'EF, mais il n'étend pas IQueryable et ne permet pas de remodeler la requête.

Bien sûr, si l'appelant se trouve dans le même assembly, il peut quand même convertir le IIncludable en Includable et commencer à jouer avec l'interrogable. Mais bon, si quelqu'un veut se tromper, il n'y a aucun moyen de l'empêcher de le faire (la réflexion permet tout). Ce qui importe, c'est le contrat exposé.

Maintenant, si vous ne vous souciez pas d'exposer IQueryable à l'appelant (ce dont je doute), changez simplement votre argument params pour un Func<Queryable<T>, Queryable<T>> addIncludes argument, et évitez de coder toutes ces choses ci-dessus.

Et le meilleur pour la fin: je n'ai pas testé ça, je n'utilise pas Entity Framework actuellement!

12
Frédéric

Pour la postérité, une autre solution moins éloquente, mais plus simple, qui utilise la surcharge Include() qui utilise 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;
}

Et l'appel du référentiel est:

var blog = blogRepository.GetById(id, BlogIncludes.Posts, BlogIncludes.Author);
7
im1dermike

Il y a bien sûr,

vous pouvez parcourir l'arborescence Expression des paramètres d'origine et toutes les inclusions imbriquées, ajoutez-les en tant que

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

Mais ce n'est pas anodin, mais certainement très faisable, veuillez partager si vous le faites, car il peut certainement être réutilisé!

3
Michal Ciechan

Vous pouvez faire quelque chose comme ça:

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

J'adhère à la solution plus simple qui utilise la surcharge Include () qui utilise la chaîne navigationPropertyPath. Le plus simple que je puisse écrire est cette méthode d'extension ci-dessous.

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
        }
    }
}
1
alhpe