问题
How do I filter out Grandchild elements with a Linq EF Query? This Customer has multiple transactions, and only need subchild elements with Certain ProductTypeId. Its currently bringing All ProductType Ids ignoring the filter .
var result = db.Customer
.Include(b => b.Transactions)
.Where(a => a.Transactions.Select(c=> c.ProductTypeId== productTypeId).Any())
Sql query, that I want:
select distinct c.customerName
from dbo.customer customer
inner join dbo.Transactions transaction
on transaction.customerid = customer.customerid
where transaction.ProductTypeId= 5
Customer (need 7 ProductTypeId)
Transaction ProductTypeId 2
Transaction ProductTypeId 4
Transaction ProductTypeId 5
Transaction ProductTypeId 7 <--- only need this 7 in the filter, example
Transaction ProductTypeId 7 <--- only need this 7 in the filter, example
Transaction ProductTypeId 8
Transaction ProductTypeId 8
Transaction ProductTypeId 9
*Mgmt prefers Include syntax, rather than linq to Sql .
回答1:
To filter related entities you need to use projection. The purpose of an EF entity graph is to reflect the complete data state. What you want is a filtered data state. This is usually to provided relevant data to a view. That is a separate purpose.
Given a Customer/Transaction entity, use a Customer/Transaction ViewModel containing just the PKs and the properties that your view/consumer is going to need. For example:
[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; }
// ...
}
Then, when you go to load your customers & filtered transactions:
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();
Leveraging Automapper for your projections can Simplify this considerably, as you can configure the entity to view model mapping (which are one-liners if the field naming is the same) then call ProjectTo
and Automapper will resolve the fields needed for the SQL and construct the view models for you:
I.e.
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();
For the view models I would use a naming convention that reflects the view you are supplying them for as they are really only applicable to serve that view. For instance if this is reviewing transactions by customer then something like ReviewTransactionsCustomerViewModel or ReviewTransactionsCustomerVM. Different views can source different view models vs. trying to have one size fit all.
Alternatively if your code is already sending Entities to views (which I strongly discourage) there are a couple alternatives, but these do have drawbacks:
- Using a wrapper view model with a filtered sub-set:
For example:
[Serializable]
public class ReviewTransactionsViewModel
{
public Customer Customer { get; set; }
public ICollection<Transaction> ApplicableTransactions { get; set; } = new List<Transaction>();
}
Then when selecting:
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();
Then in your view, instead of the @Model being a Customer, it becomes this view model and you just need to tweak any references to use Model.Customer.{property}
rather than Model.{property}
and importantly, any references to Model.Transactions
should be updated to Model.ApplicableTransactions
, not Model.Customer.Transactions
.
The caveat to this approach is that for performance you should disable lazy loading on the DbContext instance populating your model to send back, and only eager-load the data your view will need. Lazy loading will get tripped by code serializing entities to send to a view which can easily be a major performance hit. This means any references to Model.Customer.Transactions
will be empty. It also means that your model will not represent a complete entity, so when passing this model back to the controller you need to be aware of this fact and not attempt to attach it to use as a complete entity or pass to a method expecting a complete entity.
- Filter the data into a new entity: (Treat entity as view model)
For example:
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();
This can be an attractive option as it requires no changes to the consuming view but you must be cautious as this model now does not reflect a complete data state when/if passed back to the controller or any method that may assume that a provided Customer is a complete representation or a tracked entity. I believe you can leverage an Automapper confguration for <Customer, Customer>
to help facilitate the filtering and copying across only applicable columns, ignoring unneeded related entities etc.
In any case, this should give you some options to weigh up risks vs. effort.
回答2:
a better approach is to use Transactions dbset to start the query with Include Customer and then apply your filters on transactions, after that Select customerName from results with Distinct expression to get unique customers name.
回答3:
So, try to write queries as you expect them from SQL.
var namesAll =
from customer in db.Customer
from transaction in customer.Transactions
where transaction.ProductTypeId == 5
select customer.CustomerName;
var result = namesAll.Distinct();
Lambda syntax (method chain), IMHO which is worst readable.
var result = db.Customer
.SelectMany(customer => customer.Transactions,
(customer, transaction) => new {customer, transaction})
.Where(pair => pair.transaction.ProductTypeId == 5)
.Select(pair => pair.customer.CustomerName)
.Distinct();
回答4:
If I correctly understand what do you need, try a solution like this:
My test models:
public sealed class Person
{
public Guid Id { get; set; }
public DateTime? Deleted { get; set; }
public string Email { get; set; }
public string Name { get; set; }
public int? B { get; set; }
public IList<Vehicle> Vehicles { get; set; } = new List<Vehicle>();
}
public sealed class Vehicle
{
public Guid Id { get; set; }
public int ProductTypeId { get; set; }
public Guid PersonId { get; set; }
public Person Person { get; set; }
}
Query:
var queueItem = await _context.Persons
.Where(item => item.Vehicles.Any(i => i.ProductTypeId == 1))
.Select(item => new Person
{
Id = item.Id,
//Other props
Vehicles = item.Vehicles.Where(item2 => item2.ProductTypeId == 1).ToList()
})
.ToListAsync();
Sql from profiler:
SELECT [p].[Id], [t].[Id], [t].[PersonId], [t].[ProductTypeId]
FROM [Persons] AS [p]
LEFT JOIN (
SELECT [v].[Id], [v].[PersonId], [v].[ProductTypeId]
FROM [Vehicle] AS [v]
WHERE [v].[ProductTypeId] = 1
) AS [t] ON [p].[Id] = [t].[PersonId]
WHERE EXISTS (
SELECT 1
FROM [Vehicle] AS [v0]
WHERE ([p].[Id] = [v0].[PersonId]) AND ([v0].[ProductTypeId] = 1))
ORDER BY [p].[Id], [t].[Id]
One more variant:
var queueItem1 = await _context.Vehicle
.Where(item2 => item2.ProductTypeId == 1)
.Include(item => item.Person)
.Distinct()
.ToListAsync();
var list = queueItem1
.GroupBy(item => item.Person)
.Select(item => new Person
{
Id = item.First().Person.Id,
//Other props
Vehicles = item.ToList()
})
.ToList();
Sql from profiler:
SELECT [t].[Id], [t].[PersonId], [t].[ProductTypeId], [p].[Id], [p].[B],
[p].[Deleted], [p].[Email], [p].[Name]
FROM (
SELECT DISTINCT [v].[Id], [v].[PersonId], [v].[ProductTypeId]
FROM [Vehicle] AS [v]
WHERE [v].[ProductTypeId] = 1
) AS [t]
INNER JOIN [Persons] AS [p] ON [t].[PersonId] = [p].[Id]
来源:https://stackoverflow.com/questions/64340622/c-sharp-entity-framework-linq-filter-out-certain-grandchild-elements