Подтвердить что ты не робот

Несколько включений() в EF Core

У меня есть метод расширения, который позволяет вам в целом включать данные в 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;
}

Это позволяет мне иметь методы в моем репозитории следующим образом:

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

Я считаю, что метод расширения работал до EF Core, но теперь, включая "children", выполняется следующим образом:

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

Есть ли способ изменить мой общий метод расширения для поддержки новой практики EF Core ThenInclude()?

4b9b3361

Ответ 1

Как сказано в комментариях других, вы можете взять код EF6 для разбора ваших выражений и применить соответствующие вызовы Include/ThenInclude. В конце концов, это выглядит не так уж и сложно, но так как это не было моей идеей, я бы предпочел не ставить ответ с кодом для этого.

Вместо этого вы можете изменить свой шаблон для предоставления некоторого интерфейса, позволяющего вам указывать ваши включения от вызывающего абонента, не давая ему доступ к базовому запросу.

Это приведет к чему-то вроде:

using YourProject.ExtensionNamespace;

// ...

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

Пространство имен using on должно соответствовать имени пространства имен, содержащему методы расширения, определенные в последнем блоке кода.

GetById был бы сейчас:

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

Метод расширения 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 классы и интерфейсы, которые являются простыми "заполнителями", для которых дополнительные методы расширений будут выполнять работу, имитирующую методы EF Include и 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;
    }
}

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> почти похож на IIncludableQueryable<TEntity, TProperty> из EF, но он не расширяет IQueryable и не позволяет IQueryable запроса.

Конечно, если вызывающая IIncludable находится в той же сборке, она все равно может привести IIncludable к Includable и начать возиться с запросом. Но хорошо, если кто-то хочет ошибиться, мы не сможем помешать ему (рефлексия позволяет все). Что имеет значение, так это выставленный контракт.

Теперь, если вы не заботитесь о предоставлении IQueryable вызывающей стороне (в чем я сомневаюсь), очевидно, просто измените аргумент params для аргумента Func<Queryable<T>, Queryable<T>> addIncludes и избегайте кодирования всех этих вещей выше.

И самое лучшее для конца: я не проверял это, я не использую Entity Framework в настоящее время!

Ответ 2

Для потомков другое менее красноречивое, но более простое решение, использующее перегрузку Include(), которая использует 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;
}

И вызов репозитория:

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

Ответ 3

Конечно, есть,

вы можете пересечь дерево выражений исходных параметров, и любые вложенные включите, добавьте их как

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

Но его нетривиальный, но, безусловно, очень выполнимый, пожалуйста, поделитесь, если вы это сделаете, поскольку его можно повторно использовать повторно.

Ответ 4

Вы можете сделать что-то вроде этого:

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

Ответ 5

Я придерживаюсь более простого решения, которое использует перегрузку Include(), которая использует строку navigationPropertyPath. Самое простое, что я могу написать, это метод расширения ниже.

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

Ответ 6

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();

    }