简体   繁体   English

foreach中的动态过滤(ASP.NET&EF)

[英]Dynamic Filtering in foreach (ASP.NET & EF)

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. 代替无聊的注释部分,我们如何构造动态/泛型逻辑以对CategoryId,MinPrice和MaxPrice进行过滤。 (For example in a foreach block of property list of ProductQuery) (例如,在ProductQuery属性列表的foreach块中)

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) 也许我们可以使用字典对象和如下所示的foreach,但我不确定如何从对象中获取属性名称作为字符串(我尝试使用NewtonSoft.JObject,但未成功)

        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: 因此,避免破坏DRY原理的最佳方法是在ProductQuery类中创建一个Filters属性,如下所示:

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 ). 通过解析表达式树(例如p => p.CategoryId == filter.CategoryId )并查看正在使用哪些filter成员(例如filter.CategoryId ),您可以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? 解析表达式的代码应该更灵活-如果filter属性在表达式中排在首位,该怎么办? 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 : 我还建议将构建单个过滤器表达式的逻辑封装为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 : 您可以自己实现,但是我强烈建议使用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;
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM