要过滤相关实体,您需要使用投影。 EF 实体图的目的是反映完整数据状态。您想要的是过滤后的数据状态。这通常是为了向视图提供相关数据。那是一个单独的目的。
给定一个客户/交易实体,使用一个客户/交易视图模型,其中只包含您的视图/消费者将需要的 PK 和属性。例如:
[Serializable]
public class CustomerViewModel
{
public int CustomerId { get; set; }
public string Name { get; set; }
// ...
public ICollection<TransactionViewModel> ApplicableTransactions { get; set; } = new List<TransactionViewModel>();
}
[Serializable]
public class TransactionViewModel
{
public int TransactionId { get; set; }
// ...
}
然后,当您加载客户和过滤交易时:
var result = db.Customer
.Where(a => a.Transactions.Select(c=> c.ProductTypeId== productTypeId).Any())
.Select(a => new CustomerViewModel
{
CustomerId = a.CustomerId,
Name = a.Name,
// ...
ApplicableTransactions = a.Transactions
.Where(c => c.ProductTypeId == productTypeId)
.Select(c => new TransactionViewModel
{
TransactionId == c.TransactionId,
// ...
}).ToList();
}).ToList();
利用 Automapper 进行投影可以大大简化这一过程,因为您可以将实体配置为查看模型映射(如果字段命名相同,则为单行),然后调用 ProjectTo,Automapper 将解析所需的字段SQL 并为您构建视图模型:
即
var mappingConfig = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Customer, CustomerViewModel>()
.ForMember(dest => dest.ApplicableTransactions,
opt => opt.MapFrom(src => src.Transactions.Where(t => t.ProductTypeId == productTypeId)
));
cfg.CreateMap<Transaction, TransactionViewModel>();
});
var result = db.Customer
.Where(a => a.Transactions.Select(c=> c.ProductTypeId== productTypeId).Any())
.ProjectTo<CustomerViewModel>(mappingConfig)
.ToList();
对于视图模型,我将使用反映您为其提供的视图的命名约定,因为它们实际上仅适用于为该视图提供服务。例如,如果这是按客户审核交易,则类似于 ReviewTransactionsCustomerViewModel 或 ReviewTransactionsCustomerVM。不同的视图可以提供不同的视图模型,而不是试图让一种尺寸适合所有人。
或者,如果您的代码已经将实体发送到视图(我强烈反对),还有一些替代方案,但它们确实有缺点:
- 使用带有过滤子集的包装视图模型:
例如:
[Serializable]
public class ReviewTransactionsViewModel
{
public Customer Customer { get; set; }
public ICollection<Transaction> ApplicableTransactions { get; set; } = new List<Transaction>();
}
然后选择:
var result = db.Customer
.Where(a => a.Transactions.Select(c=> c.ProductTypeId== productTypeId).Any())
.Select(a => new ReviewTransactionsViewModel
{
Customer = a,
ApplicableTransactions = a.Transactions
.Where(c => c.ProductTypeId == productTypeId)
.ToList();
}).ToList();
然后在您看来,@Model 不再是 Customer,而是成为此视图模型,您只需调整任何引用以使用 Model.Customer.{property} 而不是 Model.{property},重要的是,任何对 Model.Transactions 的引用都应该更新为Model.ApplicableTransactions,而不是Model.Customer.Transactions。
这种方法的注意事项是,为了提高性能,您应该禁用填充模型的 DbContext 实例上的延迟加载以发送回,并且只预先加载视图需要的数据。延迟加载将被代码序列化实体触发以发送到视图,这很容易成为主要的性能损失。这意味着对Model.Customer.Transactions 的任何引用都将为空。这也意味着您的模型不会代表一个完整的实体,因此在将此模型传递回控制器时,您需要了解这一事实,并且不要尝试将其附加为完整的实体或传递给期望完整的方法实体。
- 将数据过滤到新实体中:(将实体视为视图模型)
例如:
var result = db.Customer
.Where(a => a.Transactions.Select(c=> c.ProductTypeId== productTypeId).Any())
.Select(a => new Customer
{
CustomerId = a.CustomerId,
Name = a.Name,
// ... Just the values the view will need.
Transactions = a.Transactions
.Where(c => c.ProductTypeId == productTypeId)
.ToList();
}).ToList();
这可能是一个有吸引力的选项,因为它不需要更改消费视图,但您必须小心,因为当/如果传递回控制器或任何可能假设前提是客户是完整的代表或被跟踪的实体。我相信您可以利用 <Customer, Customer> 的 Automapper 配置来帮助促进过滤和复制仅适用的列,忽略不需要的相关实体等。
无论如何,这应该为您提供一些权衡风险与努力的选项。