为什么不在 OnActionExecuting() 方法中将 ActionContext.Response 设置为 BadRequest 并将其直接返回给调用者?
Why isn't setting the ActionContext.Response to BadRequest in the OnActionExecuting() method returning it straight back to the caller?
我编写了一个 ActionFilter,它检查传递给任何给定 [Web API] 操作方法的指定字符串参数的长度,如果长度不正确,则设置 ActionContext.Response 到 HttpStatusCode.BadRequest(通过调用 actionContext.Request.CreateErrorResponse()),但我仍然在我的操作方法代码中结束。这基本上意味着像人们创建的所有那些 ActionFilterAttribute classes 一样工作,以在操作方法之外处理 ModelState 的验证,但我也需要依赖注入以便我可以使用记录器,并且让我的 Attribute/ActionFilter 可以测试。
我的搜索找到了这个博客 post,其中作者描述了一种拥有 "passive Attribute"(其中 Attrib 基本上只是一个 DTO)和 'scanning' ActionFilter 的方法实现所述属性的行为。
https://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=98
我遇到的问题如下;
(对不起各位,请耐心等待。虽然我有多年的 C# 经验,但这是我第一次真正涉足 Attribute(s) and/or ActionFilter(s))
我已经将我的属性编写为被动属性(其中属性只是一个 DTO),以及一个继承自 IActionFilter< CheckStringParamLengthAttribute > 的 ActionFilter,如上述博客 post 中的示例所示.
这是我的属性代码。
[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 设置为 'error 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' 将被发送回调用者和放置我的属性的操作方法甚至不会被处决。令人沮丧的是,事实并非如此。
所以问题是,为了在不进入操作方法的情况下将 Bad Request 直接发送回调用者,我缺少什么?或者就此而言,这可能吗?
紧要关头,我总是可以将另一个 'service layer' class 实例注入控制器,并在每个需要调用 a/the 字符串参数的操作方法中都有代码长度验证器,但至少在我开始时,这似乎是一种更好、更简洁的方法。
更新:好吧,该死!我显然忘记了最重要的部分。
我知道这个,因为,好吧,请看下面的答案。
同时,这里是ActionFilterDispatcher,它注册在Global.asax.cs的Application_Start()方法中。例如
protected void Application_Start()
{
...
GlobalConfiguration.Configuration.Filters.Add(
new ActionFilterDispatcher(container.GetAllInstances));
...
}
已注册的 ActionFilter 是从此 class 的 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; } }
}
为了在应得的信用点上给予信用,一位伟大且非常有帮助的开发人员 [来自 efnet 上的#asp.net 频道] 给了我这个问题的答案。
因为 ActionFilter 是从这个 class 的 ExecuteActionFilterAsync() 方法调用的,所以我需要添加一个非常简单的 if 语句来检查 HttpActionContext.Response 对象是否已被填充,并且如果是这样,立即退出,然后将创建的响应立即发送回调用者。
这是固定的方法。
public sealed class ActionFilterDispatcher : IActionFilter
{
...
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);
// ADDED THIS in order to send my BadRequest response right
// back to the caller [of the Web API endpoint]
if (context.Response != null)
{
return Task.FromResult(context.Response);
}
}
}
return continuation();
}
...
}
我编写了一个 ActionFilter,它检查传递给任何给定 [Web API] 操作方法的指定字符串参数的长度,如果长度不正确,则设置 ActionContext.Response 到 HttpStatusCode.BadRequest(通过调用 actionContext.Request.CreateErrorResponse()),但我仍然在我的操作方法代码中结束。这基本上意味着像人们创建的所有那些 ActionFilterAttribute classes 一样工作,以在操作方法之外处理 ModelState 的验证,但我也需要依赖注入以便我可以使用记录器,并且让我的 Attribute/ActionFilter 可以测试。
我的搜索找到了这个博客 post,其中作者描述了一种拥有 "passive Attribute"(其中 Attrib 基本上只是一个 DTO)和 'scanning' ActionFilter 的方法实现所述属性的行为。 https://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=98
我遇到的问题如下;
(对不起各位,请耐心等待。虽然我有多年的 C# 经验,但这是我第一次真正涉足 Attribute(s) and/or ActionFilter(s))
我已经将我的属性编写为被动属性(其中属性只是一个 DTO),以及一个继承自 IActionFilter< CheckStringParamLengthAttribute > 的 ActionFilter,如上述博客 post 中的示例所示.
这是我的属性代码。
[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 设置为 'error 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' 将被发送回调用者和放置我的属性的操作方法甚至不会被处决。令人沮丧的是,事实并非如此。
所以问题是,为了在不进入操作方法的情况下将 Bad Request 直接发送回调用者,我缺少什么?或者就此而言,这可能吗?
紧要关头,我总是可以将另一个 'service layer' class 实例注入控制器,并在每个需要调用 a/the 字符串参数的操作方法中都有代码长度验证器,但至少在我开始时,这似乎是一种更好、更简洁的方法。
更新:好吧,该死!我显然忘记了最重要的部分。
我知道这个,因为,好吧,请看下面的答案。
同时,这里是ActionFilterDispatcher,它注册在Global.asax.cs的Application_Start()方法中。例如
protected void Application_Start()
{
...
GlobalConfiguration.Configuration.Filters.Add(
new ActionFilterDispatcher(container.GetAllInstances));
...
}
已注册的 ActionFilter 是从此 class 的 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; } }
}
为了在应得的信用点上给予信用,一位伟大且非常有帮助的开发人员 [来自 efnet 上的#asp.net 频道] 给了我这个问题的答案。
因为 ActionFilter 是从这个 class 的 ExecuteActionFilterAsync() 方法调用的,所以我需要添加一个非常简单的 if 语句来检查 HttpActionContext.Response 对象是否已被填充,并且如果是这样,立即退出,然后将创建的响应立即发送回调用者。
这是固定的方法。
public sealed class ActionFilterDispatcher : IActionFilter
{
...
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);
// ADDED THIS in order to send my BadRequest response right
// back to the caller [of the Web API endpoint]
if (context.Response != null)
{
return Task.FromResult(context.Response);
}
}
}
return continuation();
}
...
}