Im working on a web api project. Im using Dependency Injection so im creating all my methods in my ProductRepository
class. Specificly public IQueryable<Product> GetTwoWithLowestPrice()
method.
What i need to achieve is returning 2 products with the lowest price from the database.
I tried to get all the products from the database and sort them ascending, which would mean that 2 products with lowest price would be at the beginning of this sorted
list. Hence I'm adding products on index
position 0 and 1 (first two) to my retVal
list... Im probably not allowed to cast lists into different collections so easily without losing data (im not THAT familiar with every collection out there). Code:
public IQueryable<Product> GetTwoWithLowestPrice()
{
List<Product> retVal = new List<Product>();
var sorted = db.Products.Include(p => p.Category).OrderBy(p => p.Price);
var list = sorted as List<Product>;
retVal.Add(list[0]);
retVal.Add(list[1]);
return retVal as IQueryable<Product>;
}
This throws exception
('Object reference not set to an instance of an object.' list was null.)
Your code example looks to be mix-mashing a number of techniques/practices unnecessarily.
First off:
public IQueryable<Product> GetTwoWithLowestPrice()
IQueryable
is an interface to expose data in a way that Linq can query across. It is an extremely powerful type that can allow you to simplify return types, especially EF Entities in a manner that callers which understand the data can further reduce or consume it in any number of ways. Normally this would be used in something like a Repository pattern for a method like:
public IQueryable<Product> GetProducts()
or even:
public IQueryable<Product> GetProductById(int productId)
The reason for having a method like this can be to centralize fundamental rules about your data. Such as returning only Active products by default, or performing base level rules like enforcing a user can only receive products that apply to them. It may seem strange to return IQueryable<Product>
even for a method that is expected to return a single product, but it allows the caller to streamline the query:
Does a product exist?
bool exists = repository.GetProductById(productId).Any();
Rather than using EF to load an entire entity just to later check if it's Null or not.
It also allows you to project the data using Select
to streamline the query down to selecting just the data you care about. I.e. if you're populating a list of products and just need the product ID, Name, and price:
var productList = repository.GetProducts()
.Select(x => new ProductSummaryViewModel
{
ProductId = x.ProductId,
Name = x.Name,
Price = x.Price
}).ToList();
Add to that support for sorting, pagination, etc. The same applies to the "ById", you can leverage IQueryable
to project the entity and its relatives down to just the values you need.
Given you have a method that is designed to only return the 2 cheapest products, IQueryable
isn't really a useful return type. I'd just return an IEnumerable
.
public IEnumerable<Product> GetTwoWithLowestPrice()
{
The next thing would be to leverage Linq to Order and Take the two cheapest products. For your code:
var list = sorted as List<Product>;
retVal.Add(list[0]);
retVal.Add(list[1]);
at the bare minimum would have needed to be:
var list = sorted.ToList();
retVal.Add(list[0]);
retVal.Add(list[1]);
However, this is a very costly mistake as you are telling EF to return ALL Products when you only want the first two. Linq and EF can accommodate this with Take
.
public IEnumerable<Product> GetTwoWithLowestPrice()
{
var products = db.Products
.Include(p => p.Category)
.OrderBy(p => p.Price)
.Take(2)
.ToList();
return products;
}
And that is it. This will return the 0-2 cheapest products, including their category, and the query will only return those 0-2 rows.
Overall this is a very specific scenario method, and if this is a repository I'd probably consider leaving the specific scenario logic in the controller rather than having a growing number of specific methods in the data translation layer. This requires elevating the DbContext or Unit of Work to the Controller/Presenter.
So using the previous IQueryable<Product> GetProducts()
in a repository, the controller code that wants the cheapest two products would look like:
var products = _repository.GetProducts()
.OrderBy(x => x.Price)
.Select(x => new ProductSummaryViewModel
{
ProductId = x.ProductId,
Name = x.Name,
Price = x.Price,
CategoryName = x.Category.Name
}).Take(2)
.ToList();
The advantage of this will be that projection (Select
) will build a query that only retrieves the necessary fields, including related ones (x.Category.Name) rather than loading all columns from Product and its associated Category. (note that .Include
is not needed when using Select
) I highly recommend using ViewModels that contain just enough info that the view needs rather than returning entire entities, especially with related entities to views. It saves memory, results in faster queries, and are faster to transfer over the wire. It also exposes less about your data structure for would-be attackers.
That's a bit of a run down on how to leverage EF and Linq to get desired sets of data.
Thank you, this explains a lot. I knew my approach was bad definitely. Question: I will also have other methods ex
GetAllWithPriceLowerThan(decimal price)
orGetAllByCategoryId(int categoryId)
.Is usingIEnumerable<Product>
as areturn value
for the mentioned the way to go?You can use either, though it's worth considering how much data your system will eventually have. If it's in the 100's or possibly low 1000's of products then
IEnumerable
is Ok, but as systems grow you'll arguably want to consider adding pagination to the queries. AdoptingIQueryable
at a low level can help there rather than trying to pass criteria for page #, page size, along with what details to eager load, sort orders, etc.So IEnumerable is more often used to iretate through Lists and while it is good to use IQueryable if working with dbSets/context since it doesnt really take the unneccesary data before filtering. Also im building web api single page app so there are no views. That being said when you say
select (x => ProductSunmaryViewModel ...
i should look at that as a data transfer object wich i need to create in Models folder, right?. Also i need to return Products WITH their category properties. I would then just add Category object as a prooerty to my "vm" aka DTO and then define info inselect
part?Yep, that is pretty much the gist of it. :) The Entities reflect the data model as it's stored. The ViewModels reflect the model as the view needs it to be. Projection is the glue that can work with EF's
IQueryable
to use the Entity mappings to build efficient queries to populate a model for the view. Automapper hasProjectTo
which links intoIQueryable
to build a suitable query and return you the ViewModels once configured.