Управление тем, что возвращается с запросом $expand

Таким образом, используя ODataController, вы можете контролировать, что будет возвращено, если кто-то выполнит /odata/Foos(42)/Bars, потому что вас вызовут на FoosController следующим образом:

public IQueryable<Bar> GetBars([FromODataUri] int key) { }

Но что, если вы хотите контролировать, что будет возвращено, когда кто-то сделает /odata/Foos?$expand=Bars? Как ты с этим справляешься? Он запускает этот метод:

public IQueryable<Foo> GetFoos() { }

И я предполагаю, что он просто делает .Include("Bars") на IQueryable<Foo>, который вы возвращаете, так что... как мне получить больше контроля? В частности, как мне сделать так, чтобы OData не ломался (т. е. такие вещи, как $select, $orderby, $top и т. д., продолжали работать).


person Alex    schedule 29.09.2015    source источник


Ответы (2)


Хотя это и не то решение, которое я хотел (сделайте это встроенной функцией, ребята!), я нашел способ сделать то, что хотел, хотя и в несколько ограниченном виде (пока что я поддерживаю только прямую фильтрацию Where()).

Во-первых, я создал собственный класс ActionFilterAttribute. Его цель состоит в том, чтобы принять меры после того, как EnableQueryAttribute сделал свое дело, поскольку он изменяет запрос, созданный EnableQueryAttribute.

В вызове GlobalConfiguration.Configure(config => { ... }) добавьте следующее перед вызовом config.MapODataServiceRoute():

config.Filters.Add(new NavigationFilterAttribute(typeof(NavigationFilter)));

Это должно быть раньше, потому что методы OnActionExecuted() вызываются в обратном порядке. Вы также можете украсить определенные контроллеры этим фильтром, хотя мне было сложнее обеспечить, чтобы он работал в правильном порядке таким образом. NavigationFilter — это класс, который вы создаете сами, я опубликую пример одного из них ниже.

NavigationFilterAttribute и его внутренний класс ExpressionVisitor относительно хорошо документированы с комментариями, поэтому я просто вставлю их ниже без дополнительных комментариев:

public class NavigationFilterAttribute : ActionFilterAttribute
{
    private readonly Type _navigationFilterType;

    class NavigationPropertyFilterExpressionVisitor : ExpressionVisitor
    {
        private Type _navigationFilterType;

        public bool ModifiedExpression { get; private set; }

        public NavigationPropertyFilterExpressionVisitor(Type navigationFilterType)
        {
            _navigationFilterType = navigationFilterType;
        }

        protected override Expression VisitMember(MemberExpression node)
        {
            // Check properties that are of type ICollection<T>.
            if (node.Member.MemberType == System.Reflection.MemberTypes.Property
                && node.Type.IsGenericType
                && node.Type.GetGenericTypeDefinition() == typeof(ICollection<>))
            {
                var collectionType = node.Type.GenericTypeArguments[0];

                // See if there is a static, public method on the _navigationFilterType
                // which has a return type of Expression<Func<T, bool>>, as that can be
                // handed to a .Where(...) call on the ICollection<T>.
                var filterMethod = (from m in _navigationFilterType.GetMethods()
                                    where m.IsStatic
                                    let rt = m.ReturnType
                                    where rt.IsGenericType && rt.GetGenericTypeDefinition() == typeof(Expression<>)
                                    let et = rt.GenericTypeArguments[0]
                                    where et.IsGenericType && et.GetGenericTypeDefinition() == typeof(Func<,>)
                                        && et.GenericTypeArguments[0] == collectionType
                                        && et.GenericTypeArguments[1] == typeof(bool)

                                    // Make sure method either has a matching PropertyDeclaringTypeAttribute or no such attribute
                                    let pda = m.GetCustomAttributes<PropertyDeclaringTypeAttribute>()
                                    where pda.Count() == 0 || pda.Any(p => p.DeclaringType == node.Member.DeclaringType)

                                    // Make sure method either has a matching PropertyNameAttribute or no such attribute
                                    let pna = m.GetCustomAttributes<PropertyNameAttribute>()
                                    where pna.Count() == 0 || pna.Any(p => p.Name == node.Member.Name)
                                    select m).SingleOrDefault();

                if (filterMethod != null)
                {
                    // <node>.Where(<expression>)
                    var expression = filterMethod.Invoke(null, new object[0]) as Expression;
                    var whereCall = Expression.Call(typeof(Enumerable), "Where", new Type[] { collectionType }, node, expression);
                    ModifiedExpression = true;
                    return whereCall;
                }
            }
            return base.VisitMember(node);
        }
    }

    public NavigationFilterAttribute(Type navigationFilterType)
    {
        _navigationFilterType = navigationFilterType;
    }

    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
    {
        HttpResponseMessage response = actionExecutedContext.Response;

        if (response != null && response.IsSuccessStatusCode && response.Content != null)
        {
            ObjectContent responseContent = response.Content as ObjectContent;
            if (responseContent == null)
            {
                throw new ArgumentException("HttpRequestMessage's Content must be of type ObjectContent", "actionExecutedContext");
            }

            // Take the query returned to us by the EnableQueryAttribute and run it through out
            // NavigationPropertyFilterExpressionVisitor.
            IQueryable query = responseContent.Value as IQueryable;
            if (query != null)
            {
                var visitor = new NavigationPropertyFilterExpressionVisitor(_navigationFilterType);
                var expressionWithFilter = visitor.Visit(query.Expression);
                if (visitor.ModifiedExpression)
                    responseContent.Value = query.Provider.CreateQuery(expressionWithFilter);
            }
        }
    }
}

Далее, есть несколько простых классов атрибутов, с целью сузить фильтрацию.

Если вы поместите PropertyDeclaringTypeAttribute в один из методов вашего NavigationFilter, он будет вызывать этот метод только в том случае, если свойство относится к этому типу. Например, если у вас есть класс Foo со свойством типа ICollection<Bar>, если у вас есть метод фильтра со свойством [PropertyDeclaringType(typeof(Foo))], то он будет вызываться только для свойств ICollection<Bar> в Foo, но не для любого другого класса.

PropertyNameAttribute делает что-то подобное, но для имени свойства, а не для типа. Это может быть полезно, если у вас есть тип объекта с несколькими свойствами одного и того же ICollection<T>, где вы хотите фильтровать по-разному в зависимости от имени свойства.

Они здесь:

[AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class PropertyDeclaringTypeAttribute : Attribute
{
    public PropertyDeclaringTypeAttribute(Type declaringType)
    {
        DeclaringType = declaringType;
    }

    public Type DeclaringType { get; private set; }
}

[AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class PropertyNameAttribute : Attribute
{
    public PropertyNameAttribute(string name)
    {
        Name = name;
    }

    public string Name { get; private set; }
}

Наконец, вот пример класса NavigationFilter:

class NavigationFilter
{
    [PropertyDeclaringType(typeof(Foo))]
    [PropertyName("Bars")]
    public static Expression<Func<Bar,bool>> OnlyReturnBarsWithSpecificSomeValue()
    {
        var someValue = SomeClass.GetAValue();
        return b => b.SomeValue == someValue;
    }
}
person Alex    schedule 05.10.2015
comment
Я делаю что-то подобное. Я боялся, что мне придется изменить каждый запрос, но делать это в действии приятно. Что бы это ни стоило, есть FilterQueryValidator, который выглядит многообещающе, но я не уверен, что нужно изменять данный запрос внутри *Validator. asp.net/ web-api/overview/odata-support-in-aspnet-web-api/ - person ta.speot.is; 02.11.2015
comment
ODataQueryOptions тоже выглядит многообещающе. В любом случае, плюс один и спасибо, что поделились реализацией. - person ta.speot.is; 02.11.2015
comment
@ ta.speot.is Да, я посмотрел на оба из них, но ни один не сделал то, что мне было нужно. В конечном итоге я обнаружил, что мне нужно изменить сам запрос, что я и сделал. Если вы найдете менее хакерский способ сделать это, дайте мне знать. :) - person Alex; 02.11.2015
comment
@Alex Поверь, у тебя все хорошо. Не могли бы вы поделиться своим опытом в этом, пожалуйста, заголовок ="группировать по расширению для веб-api odata asp net"> stackoverflow.com/questions/46319844/ - person Abhijeet; 03.11.2017

@Алекс

1) Вы можете добавить параметр в GetBars(... int key) и использовать параметр, чтобы сделать больше контроллера для опции запроса. Например,

public IQueryable<Bar> GetBars(ODataQueryOptions<Bar> options, [FromODataUri] int key) { }

2) Или вы можете добавить [EnableQuery] к действию GetBars, чтобы позволить OData веб-API выполнять параметры запроса.

[EnableQuery]
public IQueryable<Bar> GetBars([FromODataUri] int key) { }
person Sam Xu    schedule 30.09.2015
comment
Ни один из этих вариантов не является ответом на мой вопрос. Я не хочу изменять принцип работы GetBars. Тот работает, как и ожидалось. Моя проблема в том, что я хочу иметь возможность контролировать то, что возвращается из Bars, когда кто-то делает /odata/Foos?$expand=Bars. - person Alex; 30.09.2015
comment
GetBars(), к сожалению, в этом случае не вызывается. - person Alex; 30.09.2015