Динамическая фильтрация в foreach (ASP.NET & EF) - PullRequest
0 голосов
/ 14 мая 2018

У меня очень простой случай с контроллером и хранилищем.

Контроллер:

    [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);
    }

Репозиторий:

    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);
    }

Модель:

    public class ProductQuery
    {
        public int? CategoryId { get; set; }
        public decimal? MinPrice { get; set; }
        public decimal? MaxPrice { get; set; }
    }

Вместо скучной закомментированной части, как мы можем структурировать динамическую / общую логику, чтобы выполнять фильтрацию для CategoryId, MinPrice и MaxPrice.(Например, в блоке foreach списка свойств ProductQuery)

Возможно, мы можем использовать объект словаря и 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);
        }

Я не хочу использовать отражение.Также приветствуются идеи или комментарии с лучшими практиками в таком случае.

Ответы [ 2 ]

0 голосов
/ 14 мая 2018

То, что я сделал, работает, да, и я могу продолжать так же, как это, но это будет результат много дублированной логики. И потому что это игрушечный проект, я ищу способы его улучшить. И такой проект, это излишне я согласен ..

Таким образом, лучший способ избежать нарушения принципа DRY - это создать свойство Filters в классе ProductQuery, например:

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;
        }
    }
}

Так что в своем коде вы можете использовать его, как показано ниже:

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);
}
0 голосов
/ 14 мая 2018

Возможно, вы могли бы использовать кортеж значения пар тест-функция и выражение:

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);
    }
}

Вы можете пойти еще дальше, проанализировав дерево выражений (например, p => p.CategoryId == filter.CategoryId) и увидев, какой элемент (ы) из filter используется (например, filter.CategoryId). Тогда вы можете применить условие, только если этот член имеет значение:

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);
    }
}

Таким образом, вы можете избежать определения нулевой проверки.

Код, который анализирует выражения, вероятно, должен быть более гибким - что, если свойство фильтра будет первым в выражении? Что, если где-то есть конверсии?


Я бы также предложил инкапсулировать логику построения выражения одного фильтра как свойство ProductQuery:

public Expression<Product, bool> Filter => {
    get {
        // implementation at end of answer
    }
}

, который можно затем вызывать без каких-либо петель:

products = products.Where(filter.Filter);

Вы можете реализовать это самостоятельно, но я настоятельно рекомендую использовать 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;
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...