控制 $expand 请求返回的内容
Controlling what is returned with an $expand request
因此,使用 ODataController
,您可以控制在有人 /odata/Foos(42)/Bars
时 return 编辑的内容,因为您会在 FoosController
上被调用所以:
public IQueryable<Bar> GetBars([FromODataUri] int key) { }
但是,如果您想要控制在某人 /odata/Foos?$expand=Bars
时 return 编辑的内容怎么办?你怎么处理那件事呢?它触发这个方法:
public IQueryable<Foo> GetFoos() { }
而且我假设它只是在 IQueryable<Foo>
上执行 .Include("Bars")
而您 return,所以...我如何获得更多控制权?特别是,我如何以不破坏 OData 的方式进行操作(即 $select、$orderby、$top 等继续工作。)
@Alex
1) 您可以在 GetBars(... int key) 中添加一个参数,并使用该参数为查询选项做更多的控制器。例如,
public IQueryable<Bar> GetBars(ODataQueryOptions<Bar> options, [FromODataUri] int key) { }
2) 或者,您可以在操作 GetBars
上添加 [EnableQuery],让 Web API OData 执行查询选项。
[EnableQuery]
public IQueryable<Bar> GetBars([FromODataUri] int key) { }
虽然这不是我想要的解决方案(让这成为一个内置功能,伙计们!),但我找到了一种方法来做我想做的事,尽管方式有点有限(到目前为止我只支持直接 Where()
过滤)。
首先,我做了一个自定义ActionFilterAttribute
class。它的目的是在 EnableQueryAttribute
完成它的事情后 采取行动,因为它修改了 EnableQueryAttribute
产生的查询。
在您的 GlobalConfiguration.Configure(config => { ... })
调用中,在 对 config.MapODataServiceRoute()
的调用之前添加以下内容:
config.Filters.Add(new NavigationFilterAttribute(typeof(NavigationFilter)));
必须是之前,因为OnActionExecuted()
方法调用的顺序是相反的。你也可以用这个过滤器装饰特定的控制器,虽然我发现很难确保它是 运行 以正确的顺序。 NavigationFilter
是一个class你自己创建,我再往下post举例一个。
NavigationFilterAttribute
和它的内部 class,一个 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);
}
}
}
}
接下来,有几个简单的属性classes,用于缩小过滤范围。
如果您将 PropertyDeclaringTypeAttribute
放在 NavigationFilter
的其中一个方法上,它只会在 属性 属于该类型时调用该方法。例如,给定 class Foo
和类型 ICollection<Bar>
的 属性,如果你有一个 [PropertyDeclaringType(typeof(Foo))]
的过滤器方法,那么它只会被调用ICollection<Bar>
Foo
的属性,但不适用于任何其他 class。
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 的例子:
class NavigationFilter
{
[PropertyDeclaringType(typeof(Foo))]
[PropertyName("Bars")]
public static Expression<Func<Bar,bool>> OnlyReturnBarsWithSpecificSomeValue()
{
var someValue = SomeClass.GetAValue();
return b => b.SomeValue == someValue;
}
}
因此,使用 ODataController
,您可以控制在有人 /odata/Foos(42)/Bars
时 return 编辑的内容,因为您会在 FoosController
上被调用所以:
public IQueryable<Bar> GetBars([FromODataUri] int key) { }
但是,如果您想要控制在某人 /odata/Foos?$expand=Bars
时 return 编辑的内容怎么办?你怎么处理那件事呢?它触发这个方法:
public IQueryable<Foo> GetFoos() { }
而且我假设它只是在 IQueryable<Foo>
上执行 .Include("Bars")
而您 return,所以...我如何获得更多控制权?特别是,我如何以不破坏 OData 的方式进行操作(即 $select、$orderby、$top 等继续工作。)
@Alex
1) 您可以在 GetBars(... int key) 中添加一个参数,并使用该参数为查询选项做更多的控制器。例如,
public IQueryable<Bar> GetBars(ODataQueryOptions<Bar> options, [FromODataUri] int key) { }
2) 或者,您可以在操作 GetBars
上添加 [EnableQuery],让 Web API OData 执行查询选项。
[EnableQuery]
public IQueryable<Bar> GetBars([FromODataUri] int key) { }
虽然这不是我想要的解决方案(让这成为一个内置功能,伙计们!),但我找到了一种方法来做我想做的事,尽管方式有点有限(到目前为止我只支持直接 Where()
过滤)。
首先,我做了一个自定义ActionFilterAttribute
class。它的目的是在 EnableQueryAttribute
完成它的事情后 采取行动,因为它修改了 EnableQueryAttribute
产生的查询。
在您的 GlobalConfiguration.Configure(config => { ... })
调用中,在 对 config.MapODataServiceRoute()
的调用之前添加以下内容:
config.Filters.Add(new NavigationFilterAttribute(typeof(NavigationFilter)));
必须是之前,因为OnActionExecuted()
方法调用的顺序是相反的。你也可以用这个过滤器装饰特定的控制器,虽然我发现很难确保它是 运行 以正确的顺序。 NavigationFilter
是一个class你自己创建,我再往下post举例一个。
NavigationFilterAttribute
和它的内部 class,一个 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);
}
}
}
}
接下来,有几个简单的属性classes,用于缩小过滤范围。
如果您将 PropertyDeclaringTypeAttribute
放在 NavigationFilter
的其中一个方法上,它只会在 属性 属于该类型时调用该方法。例如,给定 class Foo
和类型 ICollection<Bar>
的 属性,如果你有一个 [PropertyDeclaringType(typeof(Foo))]
的过滤器方法,那么它只会被调用ICollection<Bar>
Foo
的属性,但不适用于任何其他 class。
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 的例子:
class NavigationFilter
{
[PropertyDeclaringType(typeof(Foo))]
[PropertyName("Bars")]
public static Expression<Func<Bar,bool>> OnlyReturnBarsWithSpecificSomeValue()
{
var someValue = SomeClass.GetAValue();
return b => b.SomeValue == someValue;
}
}