Как взять фильтр конечных точек OData Queryable Web API и сопоставить его с объектом DTO? - PullRequest
3 голосов
/ 22 марта 2019

У меня есть простая конечная точка Web API, которая может принимать входящие запросы OData:

public IActionResult GetProducts(ODataQueryOptions<ProductDTO> options)
{
    var results = DomainLayer.GetProducts(options);
    return Ok(results);
}

Я специально хочу иметь возможность запрашивать у ProductDTO объектов и иметь возможность фильтровать или сортировать по свойствам представления DTO.

Моя проблема с дизайном заключается в том, что я хочу воспользоваться логикой разбора / применения фильтра библиотеки OData, но не хочу выставлять мои объекты ProductEntity, связанные с базой данных, моему веб-API AND Я не хочу возвращать IQueryable из моих DataAccessLayer, только IEnumerable с.

Затем я пытаюсь извлечь Expression из свойства FilterQueryOption входящего ODataQueryOptions, чтобы я мог использовать функцию сопоставления выражений AutoMapper для сопоставления выражения из Expression<Func<ProductDTO, bool>> в Expression<Func<Product, bool>> затем, наконец, Expression<Func<ProductEntity, bool>>, где я затем передам его в .Where() вызов на моем Table<ProductEntity> , где (надеюсь) фильтр применяется в моей базе данных SQL (через Linq-2-SQL) а затем я просто преобразовываю его обратно в объект DTO после.

Большой демонстрационный ролик, с которым я столкнулся, заключается в том, что queryable.Expression возвращает MethodCallExpression, а не Expression<Func<ProductDTO, bool>>, как я ожидал, что означает, что я не могу отобразить выражение с AutoMapper, как я планировал ...

Как я могу обойти это?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.AspNet.OData.Query;
using AutoMapper.Extensions.ExpressionMapping;
using AutoMapper.QueryableExtensions;

namespace ProductApp
{
    public class DomainLayer
    {
        public IEnumerable<ProductDTO> GetProductsByEntityOptions(ODataQueryOptions<ProductDTO> options)
        {
            var mapper = MyMapper.GetMapper();

            // This is the trick to get the expression out of the FilterQueryOption...
            IQueryable queryable = Enumerable.Empty<ProductDTO>().AsQueryable();
            queryable = options.Filter.ApplyTo(queryable, new ODataQuerySettings());            
            var exp = (MethodCallExpression) queryable.Expression;              // <-- This comes back as a MethodCallExpression...

            // Map the expression to my intermediate Product object type
            var mappedExp = mapper.Map<Expression<Func<Product, bool>>>(exp);   // <-- But I want it as a Expression<Func<ProductDTO, bool>> so I can map it...

            IEnumerable<Product> results = _dataAccessLayer.GetProducts(mappedExp);

            return mapper.Map<IEnumerable<ProductDTO>>(results);
        }
    }

    public class DataAccessLayer
    {
        public IEnumerable<Product> GetProducts(Expression<Func<Product, bool>> exp)
        {
            var mapper = MyMapper.GetMapper();

            var mappedExp = mapper.Map<Expression<Func<ProductEntity, bool>>>(exp);
            IEnumerable<ProductEntity> result = _dataContext.GetTable<ProductEntity>().Where(mappedExpression).ToList();

            return mapper.Map<IEnumerable<Product>>(result);
        }
    }
}

Ссылка:

1 Ответ

2 голосов
/ 25 марта 2019

Ну, автор принятого ответа связанного поста написал в конце:

Обратите внимание, что выражение содержит больше похоже на это, SOTests.Customer[].Where($it => conditional-expression). Поэтому вам, возможно, придется извлечь это условное выражение из лямбды.

MethodCallExpression, который вы получаете, является именно этим - «вызовом» к Queryable.Where<ProductDTO>, а лямбда-выражение Expression<Func<ProductDTO, bool>>, которое вам нужно, является вторым аргументом (помните, что Queryable.Where является static метод расширения, поэтому первый аргумент представляет IQueryable<ProductDTO>), заключенный в Expression.Quote .

Итак, все, что вам нужно, это извлечь лямбда-выражение примерно так:

public static class ODataQueryOptionsExtensions
{
    public static Expression<Func<T, bool>> GetFilter<T>(this ODataQueryOptions<T> options)
    {
        // The same trick as in the linked post
        IQueryable query = Enumerable.Empty<T>().AsQueryable();
        query = options.Filter.ApplyTo(query, new ODataQuerySettings());
        // Extract the predicate from `Queryable.Where` call
        var call = query.Expression as MethodCallExpression;
        if (call != null && call.Method.Name == nameof(Queryable.Where) && call.Method.DeclaringType == typeof(Queryable))
        {
            var predicate = ((UnaryExpression)call.Arguments[1]).Operand;
            return (Expression<Func<T, bool>>)predicate;
        }
        return null;
    }
}

и используйте его так:

public class DomainLayer
{
    public IEnumerable<ProductDTO> GetProductsByEntityOptions(ODataQueryOptions<ProductDTO> options)
    {
         var filter = options.GetFilter();
         // Here the type of filter variable is Expression<Func<ProductDTO, bool>> as desired
         // The rest ...
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...