Могу ли я иметь метод/свойство проекции, которое я могу использовать в другом методе/свойстве проекции?
У меня есть доменные сущности 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
};
@IvanStoev можно сделать с помощью AsQueryable
@joakimriedel В некоторых ограниченных сценариях да. Например, в вашем примере GetSaleProjection()
должен вызываться вне дерева выражений, выражение, хранящееся в переменной, и переменная, используемая внутри запроса. Что неприменимо, когда методу нужны другие аргументы из запроса. Короче говоря, AsQueryable()
больше похоже на обходной путь, чем на общее решение.
@IvanStoev Я знаю, что это применимо к более ранним версиям, но по крайней мере с 3.1 я мог использовать такие выражения без предварительного сохранения в переменной.
@joakimriedel Может быть, я не буду возражать. Тем не менее, это обходной путь, который работает в очень ограниченных сценариях, а также только в некоторых версиях фреймворка. Также передача связанных параметров по-прежнему невозможна. Также EFC5.0 требует, чтобы вы добавили .ToList()
, в противном случае вы получите исключение, говорящее что-то вроде того, что окончательная проекция доступна для запроса, но должна быть перечислимой и т. д. Что, конечно, является ошибкой, но просто доказывает, что обходной путь ненадежен. Решения, которые «расширяют» части дерева выражений, заменяя доступ к свойствам и вызовы методов их содержимым, не имеют таких дефектов.
@IvanStoev, с которым я согласен!
Да, но вам нужно немного изменить выражение лица.
private Expression<Func<Domain.Customers.Customer, CustomerModel>> GetEmployeeProjection()
{
return x => new CustomerModel
{
Id = x.Id,
Name = x.Name,
Sales = x.Sales.AsQueryable().Select(GetSaleProjection())
};
}
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()
.
Я не буду отмечать это как дубликат EF Core запрашивает все столбцы в SQL при сопоставлении с объектом в Select, но предлагаю взглянуть на решение там, что является одним из возможных способов (и ИМХО наиболее естественно) для решения этой распространенной проблемы с выражениями запросов и отсутствием собственного решения. При таком подходе вы должны написать код сопоставления как обычные методы расширения, а подключаемый модуль DelegateDecompiler выполнит необходимый «перевод».