2015-09-29 4 views
9

Таким образом, с помощью 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 не ломается (то есть такие вещи, как $ Выберите, $ OrderBy, $ сверху и т.д. продолжать работать.)

ответ

4

Хотя не решение, которое я хотел (сделать это встроенная функция, ребята!), Я нашел способ сделать то, что я хотел, хотя и в несколько ограниченном объеме (до сих пор я только поддерживать прямой 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; 
    } 
} 
+0

Я делаю что-то подобное. Я боялся, что мне придется модифицировать каждый запрос, но делать это в действии приятно. Для чего это стоит, есть «FilterQueryValidator», который выглядит многообещающим, но я не уверен, что нужно мутировать заданный запрос внутри '' Validator''. http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/odata-security-guidance –

+0

'ODataQueryOptions' также выглядит многообещающим. В любом случае, плюс один и спасибо за совместное использование. –

+0

@ ta.speot.is Да, я посмотрел на обоих, но не сделал то, что мне было нужно. В конечном счете, я обнаружил, что мне нужно изменить сам запрос, так что это я и сделал. Если вы найдете менее хакерский способ обойти это, сообщите мне. :) – Alex

-2

@Alex

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

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

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

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

Ни один из этих вариантов является ответом на мой вопрос. Я не хочу изменять, как работает «GetBars». Это работает так, как ожидалось. Моя проблема в том, что я хочу иметь возможность контролировать то, что возвращается с 'Bars', когда кто-то делает'/odata/Foos? $ Expand = Bars'. – Alex

+0

'GetBars()', к сожалению, не вызывается в этом экземпляре. – Alex

 Смежные вопросы

  • Нет связанных вопросов^_^