2015-09-29 14 views
9

Vì vậy, sử dụng ODataController, bạn có thể kiểm soát những gì được trả lại nếu ai đó không /odata/Foos(42)/Bars, bởi vì bạn sẽ được kêu gọi FoosController như vậy:

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

Nhưng điều gì sẽ xảy ra nếu bạn muốn kiểm soát những gì được trả về khi ai đó làm /odata/Foos?$expand=Bars? Làm thế nào để bạn đối phó với điều đó? Nó gây nên phương pháp này:

public IQueryable<Foo> GetFoos() { } 

Và tôi cho rằng nó chỉ làm một .Include("Bars") trên IQueryable<Foo> rằng bạn quay trở lại, vì vậy ... làm thế nào để tôi nhận được kiểm soát nhiều hơn? Đặc biệt, làm thế nào để tôi làm điều đó theo cách như vậy mà OData không phá vỡ (tức là những thứ như $ select, $ orderby, $ top vv tiếp tục làm việc.)

Trả lời

4

Trong khi không phải là giải pháp tôi muốn (làm cho một built-in tính năng, guys!), Tôi đã tìm thấy một cách để làm những gì tôi muốn, mặc dù trong một cách nào hạn chế (cho đến nay tôi chỉ hỗ trợ trực tiếp Where() lọc).

Trước tiên, tôi đã thực hiện một lớp tùy chỉnh ActionFilterAttribute. Mục đích của nó là để hành động sau các EnableQueryAttribute đã làm điều của nó, vì nó sửa đổi truy vấn mà EnableQueryAttribute đã tạo ra.

Trong GlobalConfiguration.Configure(config => { ... }) cuộc gọi của bạn, thêm dòng sau trước cuộc gọi đến config.MapODataServiceRoute():

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

Nó phải được trước đó, vì OnActionExecuted() phương pháp được gọi theo thứ tự ngược. Bạn cũng có thể trang trí các bộ điều khiển cụ thể với bộ lọc này, mặc dù tôi đã tìm thấy nó khó khăn hơn để đảm bảo rằng nó chạy theo đúng thứ tự như vậy. NavigationFilter là một lớp bạn tự tạo, tôi sẽ đăng một ví dụ về cách xa hơn một chút.

NavigationFilterAttribute, và lớp bên trong của nó, một ExpressionVisitor là tương đối tốt tài liệu với ý kiến, vì vậy tôi sẽ chỉ cần dán chúng mà không cần bình luận thêm dưới đây:

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

Tiếp theo, có một vài lớp thuộc tính đơn giản, cho mục đích thu hẹp bộ lọc.

Nếu bạn đặt PropertyDeclaringTypeAttribute trên một trong các phương thức trên số NavigationFilter của bạn, nó sẽ chỉ gọi phương thức đó nếu thuộc tính thuộc loại đó. Ví dụ: được cấp một lớp Foo với thuộc tính loại ICollection<Bar>, nếu bạn có phương pháp lọc với [PropertyDeclaringType(typeof(Foo))], thì nó sẽ chỉ được gọi cho ICollection<Bar> thuộc tính trên Foo, nhưng không phải cho bất kỳ lớp nào khác.

PropertyNameAttribute thực hiện điều gì đó tương tự, nhưng đối với tên thuộc tính chứ không phải là loại. Nó có thể hữu ích nếu bạn có một loại thực thể với nhiều thuộc tính của cùng một ICollection<T> nơi bạn muốn lọc khác nhau tùy thuộc vào tên thuộc tính.

Dưới đây là:

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

Cuối cùng, đây là một ví dụ về một lớp 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

Tôi đang làm một cái gì đó tương tự. Tôi sợ rằng tôi sẽ phải sửa đổi mọi truy vấn nhưng thực hiện nó trong hành động là tốt đẹp. Đối với những gì nó có giá trị, có một 'FilterQueryValidator' trông đầy hứa hẹn nhưng tôi không chắc chắn một nên thay đổi một truy vấn nhất định bên trong một' * Validator'. http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/odata-security-guidance –

+0

'ODataQueryOptions' cũng có vẻ đầy hứa hẹn. Dù bằng cách nào, cộng một và cảm ơn bạn đã chia sẻ việc triển khai. –

+0

@ ta.speot.is Vâng, tôi đã nhìn vào cả hai thứ đó, nhưng cũng không làm những gì tôi cần. Cuối cùng, tôi thấy rằng tôi cần phải sửa đổi truy vấn, vì vậy đó là những gì tôi đã kết thúc. Nếu bạn tìm thấy một cách ít hackish về nó, mặc dù, cho tôi biết. :) – Alex

-2

@ Alex

1) Bạn có thể thêm một tham số vào GetBars (... int key) và sử dụng tham số để thực hiện thêm bộ điều khiển cho tùy chọn truy vấn. ví dụ,

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

2) Hoặc Bạn có thể thêm [EnableQuery] trên hành động GetBars để cho Web API OData để làm các tùy chọn truy vấn.

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

Cả trong những lựa chọn là câu trả lời cho câu hỏi của tôi. Tôi không muốn thay đổi cách 'GetBars' hoạt động. Đó là một trong những công trình như mong đợi. Vấn đề của tôi là tôi muốn có thể kiểm soát những gì được trả về từ 'Bars' khi ai đó làm'/odata/Foos? $ Expand = Bars'. – Alex

+0

'GetBars()' không may được gọi trong trường hợp đó. – Alex

Các vấn đề liên quan