2015-09-29 6 views
9

erweitern, indem die ODataController, erhalten Sie zu kontrollieren, was zurückgegeben wird, wenn jemand /odata/Foos(42)/Bars tut, weil man auf den FoosController wie so genannt werde:

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

Aber Was ist, wenn Sie kontrollieren wollen, was zurückgegeben wird, wenn jemand /odata/Foos?$expand=Bars tut? Wie gehst du damit um? Es löst diese Methode:

public IQueryable<Foo> GetFoos() { } 

Und ich nehme an, es tut nur ein .Include("Bars") auf dem IQueryable<Foo>, die Sie zurückkommen, so ... wie kann ich mehr Kontrolle zu bekommen? Insbesondere wie mache ich es so, dass OData nicht bricht (dh Dinge wie $ wählen, $ orderby, $ top usw. weiterarbeiten.)

Antwort

4

Zwar nicht die Lösung, die ich (machen dies zu einem integrierte Funktion, Jungs!) Wollte, habe ich einen Weg gefunden, zu tun, was ich wollte, wenn auch in einer etwas eingeschränkt Weise (bisher nur ich direkt unterstützen Where() Filterung).

Zuerst machte ich eine benutzerdefinierte ActionFilterAttribute Klasse. Sein Zweck ist es, Maßnahmen zu ergreifen nach die EnableQueryAttribute hat seine Sache getan, wie es die Abfrage ändert, die EnableQueryAttribute produziert hat.

In Ihrem GlobalConfiguration.Configure(config => { ... }) Aufruf, fügen Sie den folgenden vor den Anruf zu config.MapODataServiceRoute():

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

Es hat vor sein, weil die OnActionExecuted() Verfahren in umgekehrter Reihenfolge aufgerufen werden. Sie können mit diesem Filter auch spezielle Controller einrichten, obwohl es für mich schwieriger geworden ist, sicherzustellen, dass er in der richtigen Reihenfolge ausgeführt wird. Die NavigationFilter ist eine Klasse, die Sie selbst erstellen, ich werde ein Beispiel von einem weiter unten veröffentlichen.

NavigationFilterAttribute, und seine innere Klasse wird ein ExpressionVisitor relativ gut mit Kommentaren dokumentiert, so dass ich sie einfach unten ohne weitere Kommentare einfügen:

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

Als nächstes gibt es ein paar einfache Attributklassen sind, für der Zweck, die Filterung zu verengen.

Wenn Sie PropertyDeclaringTypeAttribute auf eine der Methoden auf Ihrem NavigationFilter setzen, wird es nur diese Methode aufrufen, wenn die Eigenschaft auf diesem Typ ist. Wenn Sie zum Beispiel eine Klasse Foo mit einer Eigenschaft des Typs ICollection<Bar> haben, wenn Sie eine Filtermethode mit [PropertyDeclaringType(typeof(Foo))] haben, dann wird sie nur für ICollection<Bar> Eigenschaften auf Foo aufgerufen, aber nicht für irgendeine andere Klasse.

PropertyNameAttribute tut etwas ähnliches, aber für den Namen der Eigenschaft anstelle von Typ. Es kann nützlich sein, wenn Sie einen Entitätstyp mit mehreren Eigenschaften derselben haben, wo Sie je nach dem Namen der Eigenschaft anders filtern möchten.

Hier sind sie:

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

Schließlich, hier ist ein Beispiel für eine NavigationFilter Klasse:

class NavigationFilter 
{ 
    [PropertyDeclaringType(typeof(Foo))] 
    [PropertyName("Bars")] 
    public static Expression<Func<Bar,bool>> OnlyReturnBarsWithSpecificSomeValue() 
    { 
     var someValue = SomeClass.GetAValue(); 
     return b => b.SomeValue == someValue; 
    } 
} 
+0

Ich mache etwas ähnliches. Ich befürchtete, dass ich jede Anfrage modifizieren müsste, aber es ist schön, es in der Aktion zu tun. Für das, was es wert ist, gibt es einen 'FilterQueryValidator', der vielversprechend aussieht, aber ich bin mir nicht sicher, ob man eine gegebene Abfrage innerhalb eines' * Validators' mutieren sollte. http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/odata-security-guidance –

+0

'ODataQueryOptions' sieht auch vielversprechend aus. So oder so, plus eins und vielen Dank für die Freigabe der Implementierung. –

+0

@ ta.speot.is Ja, ich habe mir beide angeschaut, aber keiner hat getan, was ich brauchte. Letztendlich stellte ich fest, dass ich die Abfrage selbst ändern musste, also habe ich das gemacht. Wenn Sie jedoch eine weniger hetzerische Vorgehensweise finden, lassen Sie es mich wissen. :) – Alex

-2

@ Alex

1) Sie kann einen Parameter in GetBars (... int key) einfügen und den Parameter verwenden, um mehr Controller für die Abfrageoption auszuführen. zum Beispiel

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

2) Alternativ können Sie fügen [EnableQuery] auf die Aktion GetBars Web API OData lassen die Abfragemöglichkeiten zu tun.

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

Keine dieser Optionen ist eine Antwort auf meine Frage. Ich möchte nicht ändern, wie 'GetBars' funktioniert. Das funktioniert wie erwartet. Mein Problem ist, dass ich kontrollieren kann, was von 'Bars' zurückgegeben wird, wenn jemand'/odata/Foos? $ Expand = Bars' tut. – Alex

+0

'GetBars()' wird in dieser Instanz leider nicht aufgerufen. – Alex

Verwandte Themen