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;
}
}
FilterQueryValidator
that looks promising but I'm not sure one should mutate a given query inside a*Validator
. asp.net/web-api/overview/odata-support-in-aspnet-web-api/… – Etiquette