为什么不在 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();
    }
    ...
}