Pull to refresh

Избавляемся от boilerplate для валидации в ASP.NET MVC

Reading time2 min
Views11K
В большинстве примеров проверка входных данных ASP.NET MVC осуществляется следующим образом:

        [HttpPost]
        public IActionResult Test(SomeParam param)
        {
            if (!ModelState.IsValid)
            {
                return View(param);
                // return Json({success: false, state: ModelState});
            }
            
            dbContext.UpdateData(param);

            return RedirectToAction("index");
            // return Ok({success: true});
        }

Этот код можно улучшить:

  1. вынести валидацию из тела метода и избавиться от дублирования if (!ModelState.IsValid)
  2. вернуть код ответа 422

Вынесем валидацию в ActionFilter


Авторизация в ASP.NET MVC настраивается с помощью атрибутов. Сделаем по аналогии и объявим атрибут для валидации:

public enum ValidationResult
    {
        View,
        Json
    }
    
    public class ValidationFilterAttribute: ActionFilterAttribute
    {
        private readonly ValidationResult _result;

        public ValidationFilterAttribute(ValidationResult result
            = ValidationResult.Json)
        {
            _result = result;
        }

        public override void OnActionExecuting(ActionExecutingContext context)
        {
            if (!context.ModelState.IsValid)
            {
                if (_result == ValidationResult.Json)
                {
                    context.Result = new ValidationFailedResult(context.ModelState);
                }
                else
                {
                    context.Result = ((Controller)context.Controller).View(
                        context.ActionArguments.Values.First());
                    ValidationFailedResult.SetStatusCodeAndHeaders(
                        context.HttpContext);
                }
            }
        }
    }

Добавим код ответа сервера и дополнительную информацию


На stackoverflow обсуждался вопрос «какой код возвращать при ошибке валидации». Семейство 4** выглядит наиболее подходящим. 422 — уже используется Ruby из коробки. ASP.NET MVC не предлагает best practice на этот счет. Не вижу причин не привести в соответствие с Ruby:

    internal class ValidationFailedResult: JsonResult
    {
        public ValidationFailedResult(ModelStateDictionary modelState)
            : base(modelState.Select(x => new
                {
                    x.Key,
                    ValidationState = x.Value.ValidationState.ToString(),
                    x.Value.Errors
                }).ToList())        
        {
        }

        public override void ExecuteResult(ActionContext context)
        {
            base.ExecuteResult(context);
            SetStatusCodeAndHeaders(context.HttpContext);
        }

        internal static void SetStatusCodeAndHeaders(HttpContext context)
        {
            context.Response.StatusCode = 422;
            context.Response.Headers.Add("X-Status-Reason", "Validation failed");
        }
    }

Используем атрибут


ValidationFilterAttribute можно использовать на

  1. методе контроллера
  2. контроллере
  3. глобально

Остается только разделить контроллеры, возвращающие View от Json. Этого можно добиться, создав два базовых класса или добавив соглашение в атрибут, например проверять наличие api в namespace контроллера.

Примеры кода приведены для ASP.NET MVC Core. Для ASP.NET MVC придется создать два набора атрибутов для пространства имен Mvc и Http, соответственно.
Tags:
Hubs:
Total votes 22: ↑18 and ↓4+14
Comments39

Articles