I have a really simple case with a controller and a repository.
Controller:
[HttpGet]
public async Task<IActionResult> GetProductList(ProductQuery queryparams)
{
var products = await uow.ProductRepo.GetProductsWithQuery(queryparams);
var productsToReturn = mapper.Map<IEnumerable<ProductForListDto>>(products);
return Ok(productsToReturn);
}
Repository:
public async Task<AbstractPagedList<Product>>GetProductsWithQuery(ProductQuery qp)
{
var products = DorianContext.Products
.Include(p => p.Category)
.Include(p => p.PriceOffers)
.AsQueryable();
// if (filter.CategoryId.HasValue)
// products = products.Where(p => p.CategoryId == filter.CategoryId);
// if (filter.MinPrice.HasValue)
// products = products.Where(p => p.Price >= filter.MinPrice);
// if (filter.MaxPrice.HasValue)
// products = products.Where(p => p.Price <= filter.MaxPrice);
return await PagedList<Product>.CreateAsync(products, qp.PageNumber, qp.PageSize);
}
Model:
public class ProductQuery
{
public int? CategoryId { get; set; }
public decimal? MinPrice { get; set; }
public decimal? MaxPrice { get; set; }
}
Instead of the boring commented part, how can we structure a dynamic/generic logic to make filtering for CategoryId, MinPrice and MaxPrice. (For example in a foreach block of property list of ProductQuery)
Maybe we can use a dictionary object and a foreach like the following, but I am not really sure how to get Property Names as strings from the object (I tried to use NewtonSoft.JObject but without success)
var filterMap = new Dictionary<string, Expression<Func<Product, bool>>>()
{
["categoryId"] = (v => v.CategoryId == filter.CategoryId),
["collectionId"] = (v => v.ProductCollectionId == filter.CollectionId),
["minPrice"] = (v => v.Price >= filter.MinPrice),
["maxPrice"] = (v => v.Price <= filter.MaxPrice)
};
foreach (var key in filterMap)
{
products = products.Where(key.Value);
}
I don't want to use reflection. Ideas or comments with the best practices of such a case are also appreciated.
What I did works yes and I can continue just like this but this will result lots of duplicated logic. And because this is a toy project, I am searching for the ways to improve it. And such a project, this is overkill I agree..
So the best way to avoid to break the DRY principe is to create a Filters
property into ProductQuery
class like this:
public class ProductQuery
{
public int? CategoryId { get; set; }
public decimal? MinPrice { get; set; }
public decimal? MaxPrice { get; set; }
public IEnumerable<Expression<Func<Product, bool>>> Filters
{
get
{
var filters = new List<Expression<Func<Product, bool>>>();
if (this.CategoryId.HasValue)
filters.Add(p => p.CategoryId == this.CategoryId);
if (this.MinPrice.HasValue)
filters.Add((p => p.Price >= this.MinPrice);
if (this.MaxPrice.HasValue)
filters.Add(p => p.Price <= this.MaxPrice);
return filters;
}
}
}
So in your code you can use it like below:
public async Task<AbstractPagedList<Product>>GetProductsWithQuery(ProductQuery qp)
{
var products = DorianContext.Products
.Include(p => p.Category)
.Include(p => p.PriceOffers)
.AsQueryable();
foreach(var filter in qp.Filters)
{
products = products.Where(filter);
}
return await PagedList<Product>.CreateAsync(products, qp.PageNumber, qp.PageSize);
}
Perhaps you could use a value tuple, of test-function and expression pairs:
ProductQuery filter = ... // initialize here
var exprs = new List<(Func<ProductQuery, object>, Expression<Func<Product, bool>>)>() {
(f => f.CategoryId, p => p.CategoryId == filter.CategoryId),
(f => f.MinPrice, p => p.Price >= filter.MinPrice),
(f => f.MaxPrice, p => p.Price <= filter.MaxPrice)
};
foreach (var (test, expr) in exprs) {
if (test(filter) != null) {
products = products.Where(expr);
}
}
You could go even further, by parsing the expression tree (eg p => p.CategoryId == filter.CategoryId
) and seeing which member(s) of filter
are being used (eg filter.CategoryId
). Then, you could apply the condition only if that member has a value:
ProductQuery filter = ... // initialize here
var exprs = new List<Expression<Func<Product, bool>>>() {
p => p.CategoryId == filter.CategoryId,
p => p.Price >= filter.MinPrice,
p => p.Price <= filter.MaxPrice
};
foreach (var expr in exprs) {
var pi = ((expr.Body as BinaryExpression)
.Right as MemberExpression)
.Member as PropertyInfo;
if (pi.GetValue(filter) != null) {
products = products.Where(expr);
}
}
This way, you can avoid defining the null-checking test.
The code which parses the expressions should probably be more flexible -- what if the filter property is first in the expression? what if there are conversions involved somewhere?
I would also suggest encapsulating the logic of building a single filter expression as a property of ProductQuery
:
public Expression<Product, bool> Filter => {
get {
// implementation at end of answer
}
}
which you could then call without any loops:
products = products.Where(filter.Filter);
You could implement this yourself, but I highly recommend using the LINQKit PredicateBuilder :
public Expression<Func<Product, bool>> Filter {
get {
var exprs = new List<Expression<Func<Product, bool>>>() {
p => p.CategoryId == this.CategoryId,
p => p.Price >= this.MinPrice,
p => p.Price <= this.MaxPrice
}.Where(expr => {
PropertyInfo mi = ((expr.Body as BinaryExpression)
.Right as MemberExpression)
.Member as PropertyInfo;
return mi.GetValue(this) != null;
});
var predicate = PredicateBuilder.True<Product>();
foreach (var expr in exprs) {
predicate = predicate.And(expr);
}
return predicate;
}
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.