Controlling what is returned with an $expand request

后端 未结 2 720
醉话见心
醉话见心 2021-01-02 09:17

So, using the ODataController, you get to control what gets returned if somebody does /odata/Foos(42)/Bars, because you\'ll be called on the

相关标签:
2条回答
  • 2021-01-02 09:37

    @Alex

    1) You can add a parameter into GetBars(... int key) and use the parameter to do more controller for the query option. for example,

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

    2) Or, You can add [EnableQuery] on the action GetBars to let Web API OData to do the query options.

    [EnableQuery]
    public IQueryable<Bar> GetBars([FromODataUri] int key) { }
    
    0 讨论(0)
  • 2021-01-02 09:50

    While not the solution I wanted (make this a built-in feature, guys!), I have found a way to do what I wanted, albeit in a somewhat limited manner (so far I only support direct Where() filtering).

    First, I made a custom ActionFilterAttribute class. Its purpose is to take action after the EnableQueryAttribute has done its thing, as it modifies the query that EnableQueryAttribute has produced.

    In your GlobalConfiguration.Configure(config => { ... }) call, add the following before the call to config.MapODataServiceRoute():

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

    It has to be before, because the OnActionExecuted() methods are called in reverse order. You can also decorate specific controllers with this filter, although I've found it harder to ensure that it's run in the correct order that way. The NavigationFilter is a class you create yourself, I'll post an example of one farther down.

    NavigationFilterAttribute, and its inner class, an ExpressionVisitor is relatively well documented with comments, so I'll just paste them without further comments below:

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

    Next, there are a few simple attribute classes, for the purpose of narrowing down the filtering.

    If you put PropertyDeclaringTypeAttribute on one of the methods on your NavigationFilter, it will only call that method if the property is on that type. For instance, given a class Foo with a property of type ICollection<Bar>, if you have a filter method with [PropertyDeclaringType(typeof(Foo))], then it will only be called for ICollection<Bar> properties on Foo, but not for any other class.

    PropertyNameAttribute does something similar, but for the property's name rather than type. It can be useful if you have an entity type with multiple properties of the same ICollection<T> where you want to filter differently depending on the property name.

    Here they are:

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

    Finally, here's an example of a NavigationFilter class:

    class NavigationFilter
    {
        [PropertyDeclaringType(typeof(Foo))]
        [PropertyName("Bars")]
        public static Expression<Func<Bar,bool>> OnlyReturnBarsWithSpecificSomeValue()
        {
            var someValue = SomeClass.GetAValue();
            return b => b.SomeValue == someValue;
        }
    }
    
    0 讨论(0)
提交回复
热议问题