Вложение EF Core Projection в EF Core Projection

Могу ли я иметь метод/свойство проекции, которое я могу использовать в другом методе/свойстве проекции?

У меня есть доменные сущности Customer и Sale.

public class Customer : IEntity<long>
{
    public long Id { get; set; }
    public string Name { get; set; }

    public virtual ICollection<Sale> Sales { get; set; }
}

public class Sale : IEntity<long>
{
    public long Id { get; set; }
    public decimal UnitPrice { get; set; }
    public int Quantity  { get; set; }
    public decimal TotalPrice  { get; set; }
    public DateTimeOffset Date { get; set; }

    public long CustomerId { get; set; }
    public virtual Customer Customer { get; set; }

    public long EmployeeId { get; set; }
    public virtual Employee Employee { get; set; }

    public long ProductId { get; set; }
    public virtual Product Product { get; set; }

Я знаю, что могу вложить проекцию следующим образом:

public class GetCustomerQuery : IGetCustomerQuery
{
    private readonly IAppDbContext _context;

    public GetCustomerQuery(IAppDbContext context)
    {
        _context = context;
    }

    public Task<CustomerModel> ExecuteAsync(long id)
    {
        return _context
            .Customers
            .Where(x => x.Id == id)
            .Select(GetEmployeeProjection())
            .SingleOrDefaultAsync();
    }

    private Expression<Func<Domain.Customers.Customer, CustomerModel>> GetEmployeeProjection()
    {
        return x => new CustomerModel
        {
            Id = x.Id,
            Name = x.Name,
            Sales = x.Sales.Select(y => new Sale
            {
                Employee = y.Employee.Name,
                Product = y.Product.Name,
                ProductCount = y.Quantity,
                TotalPrice = y.TotalPrice
            })
        };
    }
}

Но могу ли я разделить прогнозы двумя разными способами, например:

private Expression<Func<Domain.Customers.Customer, CustomerModel>> GetEmployeeProjection()
{
    return x => new CustomerModel
    {
        Id = x.Id,
        Name = x.Name,
        Sales = x.Sales.Select(GetSaleProjection())
    };
}

private Expression<Func<Domain.Sales.Sale, Sale>> GetSaleProjection()
{
    return y => new Sale
    {
        Employee = y.Employee.Name,
        Product = y.Product.Name,
        ProductCount = y.Quantity,
        TotalPrice = y.TotalPrice
    };
}

И чаще проекцию записывают как метод или как свойство?

Обновлено: 2020-12-14 @joakimriedel, добавление .AsQueryable() действительно работает, но мне также нужно добавить .ToList(), иначе возникнет исключение времени выполнения (EF Core 5.0.1), как упоминал @Ivan Stoev.

        public Task<CustomerModel> ExecuteAsync(long id)
        {
            return _context
                .Customers
                .Where(x => x.Id == id)
                .Select(_projection)
                .SingleOrDefaultAsync();
        }

        private static readonly Expression<Func<Domain.Customers.Customer, CustomerModel>> _projection =
            x => new CustomerModel
            {
                Id = x.Id,
                Name = x.Name,
                Sales = x.Sales
                    .AsQueryable()
                    .Select(_salesProjection)
                    .ToList()
            };

        private static readonly Expression<Func<Domain.Sales.Sale, Sale>> _salesProjection =
             x => new Sale
             {
                 Employee = x.Employee.Name,
                 Product = x.Product.Name,
                 ProductCount = x.Quantity,
                 TotalPrice = x.TotalPrice
             };

Я не буду отмечать это как дубликат EF Core запрашивает все столбцы в SQL при сопоставлении с объектом в Select, но предлагаю взглянуть на решение там, что является одним из возможных способов (и ИМХО наиболее естественно) для решения этой распространенной проблемы с выражениями запросов и отсутствием собственного решения. При таком подходе вы должны написать код сопоставления как обычные методы расширения, а подключаемый модуль DelegateDecompiler выполнит необходимый «перевод».

Ivan Stoev 11.12.2020 17:19

@IvanStoev можно сделать с помощью AsQueryable

joakimriedel 14.12.2020 09:35

@joakimriedel В некоторых ограниченных сценариях да. Например, в вашем примере GetSaleProjection() должен вызываться вне дерева выражений, выражение, хранящееся в переменной, и переменная, используемая внутри запроса. Что неприменимо, когда методу нужны другие аргументы из запроса. Короче говоря, AsQueryable() больше похоже на обходной путь, чем на общее решение.

Ivan Stoev 14.12.2020 10:56

@IvanStoev Я знаю, что это применимо к более ранним версиям, но по крайней мере с 3.1 я мог использовать такие выражения без предварительного сохранения в переменной.

joakimriedel 14.12.2020 11:01

@joakimriedel Может быть, я не буду возражать. Тем не менее, это обходной путь, который работает в очень ограниченных сценариях, а также только в некоторых версиях фреймворка. Также передача связанных параметров по-прежнему невозможна. Также EFC5.0 требует, чтобы вы добавили .ToList(), в противном случае вы получите исключение, говорящее что-то вроде того, что окончательная проекция доступна для запроса, но должна быть перечислимой и т. д. Что, конечно, является ошибкой, но просто доказывает, что обходной путь ненадежен. Решения, которые «расширяют» части дерева выражений, заменяя доступ к свойствам и вызовы методов их содержимым, не имеют таких дефектов.

Ivan Stoev 14.12.2020 11:38

@IvanStoev, с которым я согласен!

joakimriedel 14.12.2020 11:44
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
6
3 021
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Да, но вам нужно немного изменить выражение лица.

EF Core 3.1

private Expression<Func<Domain.Customers.Customer, CustomerModel>> GetEmployeeProjection()
{
    return x => new CustomerModel
    {
        Id = x.Id,
        Name = x.Name,
        Sales = x.Sales.AsQueryable().Select(GetSaleProjection())
    };
}

EF Core 5.0

private Expression<Func<Domain.Customers.Customer, CustomerModel>> GetEmployeeProjection()
{
    return x => new CustomerModel
    {
        Id = x.Id,
        Name = x.Name,
        Sales = x.Sales.AsQueryable().Select(GetSaleProjection()).ToList()
    };
}

Дополнительный AsQueryable() необходим, так как IEnumerable реализация Select принимает только Func<Domain.Sales.Sale, Sale>, но IQueryable поддерживает Expression<Func<Domain.Sales.Sale, Sale>>.

Форсирование AsQueryable также полезно, если вы хотите использовать предикаты выражений в подзапросах, таких как Any().

Другие вопросы по теме