问题
I'm making an Asp.net Core Api and one of the Actions of the Controller i need to return an IQueryable of a DTO, but one of the properties is an IEnumerable of another DTO in a relationship one to many in the database model of EF. For example:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime Birthday { get; set; }
public List<Order> Orders { get; set; }
}
public class Order
{
public int OrderNumber { get; set; }
public Customer Customer { get; set; }
}
And the DTO
public class CustomerDTO
{
public int Id { get; set; }
public string Name { get; set; }
public IEnumerable<OrderDTO> Orders { get; set; }
}
public class OrderDTO
{
public int OrderNumber { get; set; }
}
This is just a simple example because in my application there is alot more fields on each table and i cannot expose everything to the frontend application, that's why i'm using DTOs.
I'm using the Select to Project each element to the DTO, there is no problem there because i can see on the ASP.NET Core Web Server output that the system is only making one request to the database (to get the Customers), but the problem comes when i try to project the OrdersDTO inside the CustomerDTO. What's happening is for example if i have 100 customers the EF will make 101 requests to the database. (1 to get the Customers and 100 to get the Orders for each customer)
[HttpGet]
[EnableQuery]
public IEnumerable<CustomerDTO> Get()
{
return context.Customer
.Select(s => new CustomerDTO
{
Id = s.Id,
Name = s.Name,
Orders = s.Orders.Select(so => new OrderDTO
{
OrderNumber = so.OrderNumber
})
});
}
If i call ToList() before i project the elements using Select it will make only one request to the Database (as intended), but i need to return an IQueryable because i'm using OData, so that the frontend application can execute queries directly to the database even if is just a DTO
I already tried putting like this
Orders = s.Orders.Any() ? s.Orders.Select(so => new OrderDTO
{
OrderNumber = so.OrderNumber
}) : new List<OrderDTO>()
It solved the problem partially because if out of the 100 customers theres is only 50 that have orders the EF will only make 50 requests to the Database.
I would like to know if there is a solution to this problem because i don't want the application doing hundreds of queries to the database each time some user calls this endpoint of the API.
回答1:
You need to add ToList()
when projecting the inner collection.
Orders = s.Orders.Select(so => new OrderDTO
{
OrderNumber = so.OrderNumber
}).ToList() // <--
First because CustomerDTO.Orders
property type is List<OrderDTO>
, so the code does not compile w/o that.
But even it wasn't (let say it's IEnumerable<OrderDTO>
), you still need ToList
in order to get EF Core 2.1 introduced Optimization of correlated subqueries
:
We have improved our query translation to avoid executing "N + 1" SQL queries in many common scenarios in which the usage of a navigation property in the projection leads to joining data from the root query with data from a correlated subquery. The optimization requires buffering the results from the subquery, and we require that you modify the query to opt-in the new behavior.
Note the last sentence - "we require that you modify the query to opt-in the new behavior". Then documentation contains an example and continues with:
By including
ToList()
in the right place, you indicate that buffering is appropriate for the Orders, which enable the optimization
来源:https://stackoverflow.com/questions/55755457/projecting-an-ienumerable-inside-an-projected-iqueryable-making-n-requests-to-th