Warm tip: This article is reproduced from serverfault.com, please click

EntityFramework / Get two objects from database with lowest price

发布于 2020-11-28 20:55:54

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.)
Questioner
Context
Viewed
0
Steve Py 2020-11-29 08:14:58

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.