Почему настройка ActionContext.Response на BadRequest в методе OnActionExecuting() не возвращает его обратно вызывающему?

#asp.net-web-api #dependency-injection #custom-attributes #action-filter

#asp.net-web-api #внедрение зависимости #пользовательские атрибуты #действие-фильтр

Вопрос:

Я написал ActionFilter, который проверяет длину указанного строкового параметра, передаваемого любому заданному методу действия [Web API], и, если длина неверна, устанавливает ActionContext.Response на HttpStatusCode.BadRequest (через вызов ActionContext.Request.CreateErrorResponse()), но я все еще заканчиваю в своем коде метода action. По сути, это должно работать подобно всем тем классам ActionFilterAttribute, которые люди создают для обработки проверки ModelState вне методов action, но мне также понадобилось внедрение зависимостей, чтобы я мог использовать регистратор и чтобы мой атрибут / ActionFilter был тестируемым.

Мой поиск привел к этому сообщению в блоге, в котором автор описывает способ иметь «пассивный атрибут» (где атрибутом является просто DTO) и «сканирующий» ActionFilter, который реализует поведение указанного атрибута. https://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=98

Проблема, с которой я сталкиваюсь, заключается в следующем;

(Извините, ребята, пожалуйста, потерпите меня. Несмотря на то, что у меня многолетний опыт работы с C #, это мой первый реальный запрос на атрибуты и / или ActionFilter (ы))

Я записал свой атрибут как пассивный (где атрибутом является просто DTO), и ActionFilter, который наследуется от IActionFilter< CheckStringParamLengthAttribute >, как показано на примерах в приведенном выше сообщении в блоге.

Вот мой код атрибута.

 [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 обратно вызывающему, и вместо этого я заканчиваю в коде моего метода действия так, как будто фильтр даже не был выполнен.

Here is the crux of my ActionFilter / ‘behavior’ code.

 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;
        }
        ...
    }
    ...
}
  

Here’s the Application_Start() method (from Global.asax.cs) showing the Simple Injector registration code, etc.

 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);
}
  

I thought that if I simply set the actionContext.Response to an ‘ErrorResponse’ that the ‘Bad Request’ would be sent back to the caller and the action method on which my attribute had been placed would not even get executed. Frustratingly, that is not the case.

Итак, вопрос в том, чего мне не хватает, чтобы отправить этот неверный запрос прямо вызывающему, не переходя к методу action? Или, если уж на то пошло, возможно ли это вообще?

Суть в том, что я всегда могу внедрить другой экземпляр класса ‘service layer’ в контроллер (ы) и иметь код в каждом методе действия, который должен вызывать средство проверки длины параметра string, но это казалось, по крайней мере, когда я начинал, лучшим и более чистым способом.

ОБНОВЛЕНИЕ: Ну, черт возьми! Я, по-видимому, забыл самую важную часть.

Я знаю это, потому что, ну, смотрите ответ ниже.

Между тем, вот ActionFilterDispatcher, который зарегистрирован в методе Application_Start() в Global.asax.cs. например

 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:

Для того, чтобы отдать должное там, где это необходимо, отличный и очень полезный разработчик [из #asp.net канал в efnet] дал мне ответ на эту проблему.

Поскольку ActionFilter вызывается из метода 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();
    }
    ...
}