我编写了一个ActionFilter来检查传递给任何给定[Web API]操作方法的指定字符串参数的长度,如果长度不正确,则将ActionContext.Response设置为HttpStatusCode.BadRequest(通过调用actionContext.Request.CreateErrorResponse()),但我仍然在我的动作方法代码中结束 . 这基本上意味着像人们创建的所有ActionFilterAttribute类一样工作,以处理动作方法之外的ModelState的验证,但我也需要依赖注入,以便我可以使用 Logger ,并拥有我的Attribute / ActionFilter是可以测试的 .
我的搜索发现了这篇博文,其中作者描述了一种方法,即"passive Attribute"(其中Attrib基本上是DTO)和'scanning' ActionFilter,它实现了所述属性的行为 . https://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=98
我遇到的问题如下;
(对不起,伙计们,请耐心等待 . 虽然我有很多年的C#经验,但这是我第一次真正进入属性和/或ActionFilter(s))
我把我的属性写成被动(其中属性只是一个DTO),以及一个继承自IActionFilter <CheckStringParamLengthAttribute>的ActionFilter,作为上述博客文章中的示例 .
这是我的属性代码 .
[AttributeUsage(AttributeTargets.Method, Inherited = true)]
public class CheckStringParamLengthAttribute : Attribute
{
private int _minLength;
private int _maxLength;
private string _errorMessage;
private string _paramName;
public CheckStringParamLengthAttribute(
int MinimumLength,
string parameterName,
string ErrorMessage = "",
int MaximumLength = 0)
{
if (MinimumLength < 0 || MinimumLength > int.MaxValue)
{
throw new ArgumentException("MinimumLength parameter value out of range.");
}
_minLength = MinimumLength;
if (string.IsNullOrEmpty(parameterName))
{
throw new ArgumentException("parameterName is null or empty.");
}
_paramName = parameterName;
// these two have defaults, so no Guard check needed.
_maxLength = MaximumLength;
_errorMessage = ErrorMessage;
}
public int MinLength { get { return _minLength; } }
public int MaxLength { get { return _maxLength; } }
public string ErrorMessage { get { return _errorMessage; } }
public string ParameterName { get { return _paramName; } }
}
..和IActionFilter声明 .
public interface IActionFilter<TAttribute> where TAttribute : Attribute
{
void OnActionExecuting(TAttribute attr, HttpActionContext ctx);
}
一切似乎都很好,直到我意识到我的ActionFilter将ActionContext.Response设置为'错误响应'...
actionContext.Response = actionContext.Request.CreateErrorResponse(
HttpStatusCode.BadRequest, "my error msg");
然后它不会将BadRequest返回给调用者,而是在我的操作方法的代码中结束,就好像过滤器甚至没有被执行一样 .
这是我的ActionFilter /'behavior'代码的关键 .
public class CheckStringParamLengthActionFilter : IActionFilter<CheckStringParamLengthAttribute>
{
...
public void OnActionExecuting(
CheckStringParamLengthAttribute attribute,
HttpActionContext actionContext)
{
Debug.WriteLine("OnActionExecuting (Parameter Name being checked: " + attribute.ParameterName + ")");
// get the attribute from the method specified in the ActionContext, if there.
var attr = this.GetStringLengthAttribute(
actionContext.ActionDescriptor);
if (actionContext.ActionArguments.Count < 1) {
throw new Exception("Invalid number of ActionArguments detected.");
}
var kvp = actionContext.ActionArguments
.Where(k => k.Key.Equals(attr.ParameterName, StringComparison.InvariantCulture))
.First();
var paramName = kvp.Key;
var stringToCheck = kvp.Value as string;
string errorMsg;
if (stringToCheck.Length < attr.MinLength) {
errorMsg = string.IsNullOrEmpty(attr.ErrorMessage)
? string.Format(
"The {0} parameter must be at least {1} characters in length.",
paramName, attr.MinLength)
: attr.ErrorMessage;
// SEE HERE
actionContext.Response = actionContext.Request.CreateErrorResponse(
HttpStatusCode.BadRequest, errorMsg);
actionContext.Response.ReasonPhrase += " (" + errorMsg + ")";
return;
}
...
}
...
}
这是Application_Start()方法(来自Global.asax.cs),显示了Simple Injector注册码等 .
protected void Application_Start()
{
// DI container spin up. (Simple Injector)
var container = new Container();
container.Options.DefaultScopedLifestyle = new WebApiRequestLifestyle();
container.Register<ILogger, Logger>(Lifestyle.Scoped);
container.RegisterWebApiControllers(GlobalConfiguration.Configuration);
GlobalConfiguration.Configuration.Filters.Add(
new ActionFilterDispatcher(container.GetAllInstances));
container.RegisterCollection(typeof(IActionFilter<>), typeof(IActionFilter<>).Assembly);
container.Verify();
GlobalConfiguration.Configuration.DependencyResolver =
new SimpleInjectorWebApiDependencyResolver(container);
// the rest of this is 'normal' Web API registration stuff.
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
我想如果我只是将actionContext.Response设置为'ErrorResponse',那么'Bad Request'将被发送回调用者,并且我的属性所在的action方法甚至都不会被执行 . 令人沮丧的是,事实并非如此 .
所以问题是,为了在不进入动作方法的情况下将错误请求直接发送回调用者,我错过了什么?或者就此而言,这甚至可能吗?
Push推进了,我总是可以将另一个'服务层'类实例注入到Controller中,并且在每个需要调用/参数字符串参数长度验证器的操作方法中都有代码,但这似乎,至少在我开始时,是一个更好,更清洁的方式 .
UPDATE: 好吧,我!我显然忘记了最重要的部分 .
我知道这一点,因为,请看下面的答案 .
同时,这里是ActionFilterDispatcher,它在Global.asax.cs的Application_Start()方法中注册 . 例如
protected void Application_Start()
{
...
GlobalConfiguration.Configuration.Filters.Add(
new ActionFilterDispatcher(container.GetAllInstances));
...
}
已注册的ActionFilter从此类的ExecuteActionFilterAsync()方法调用 . 事实上,这是关键 .
public sealed class ActionFilterDispatcher : IActionFilter
{
private readonly Func<Type, IEnumerable> container;
public ActionFilterDispatcher(Func<Type, IEnumerable> container)
{
this.container = container;
}
public Task<HttpResponseMessage> ExecuteActionFilterAsync(
HttpActionContext context,
CancellationToken cancellationToken,
Func<Task<HttpResponseMessage>> continuation)
{
var descriptor = context.ActionDescriptor;
var attributes = descriptor.ControllerDescriptor.GetCustomAttributes<Attribute>(true)
.Concat(descriptor.GetCustomAttributes<Attribute>(true));
foreach (var attribute in attributes)
{
Type filterType = typeof(IActionFilter<>).MakeGenericType(attribute.GetType());
IEnumerable filters = this.container.Invoke(filterType);
foreach (dynamic actionFilter in filters)
{
actionFilter.OnActionExecuting((dynamic)attribute, context);
}
}
return continuation();
}
public bool AllowMultiple { get { return true; } }
}
1 回答
为了给予信用到期的信誉,一位伟大且非常有帮助的开发人员[来自efnet上的#asp.net Channels ]给了我这个问题的答案 .
由于从此类的ExecuteActionFilterAsync()方法调用ActionFilter,我需要添加一个非常简单的if语句来检查并查看是否已填充HttpActionContext.Response对象,如果已填充,则立即退出,然后发送创建响应权回到调用者 .
这是固定的方法 .