Автоматическая валидация объектов POST запросов в Asp.Net Mvc Web Api

Разрабатывая серверную часть web api хочется быть уверенным, что все данные, которые необходимы для обработки конкретного запроса с клиента переданы. Что все данные проходят валидацию на соответствие требований.

Update 2016-06-27

Поиск пути

Написав несколько серверных методов web api я получив пару 500 ошибок. Дело в том, что с клиента не всегда приходят данные, которые я ожидаю. Самый простой пример это может быть опечатка в свойстве переданного объекта. Чуть посложнее: таких данных у клиента может не быть (например его текущие координаты, в случае если пользователь мобильного приложения не разрешил доступ приложению к этим данным). В любом случае получить 500 ошибки крайне неприятно. В первой итерации я получил следующую схему.

Есть интерфейс, который навешиваю на модель данных, которые жду в API.

public interface IApiFieldsValidate
{
    List<ApiFieldValidationDetails> Validate();
}

Где каждая ошибка состоит из:

public class ApiFieldValidationDetails
{
    public ApiFieldValidationDetails(string field, string message)
    {
        Field = field;
        Message = message;
    }

    [JsonProperty("field")]
    public string Field { get; set; }

    [JsonProperty("message")]
    public string Message { get; set; }
}

Хорошо. Сразу очевидно, что ошибки могут быть двух типов: данные не переданы совсем (ошибка парсера или их кто-то забыл передать) и ошибка в содержимом данных. Тогда закроем эти случаи таким способом. 

/// <summary>
/// Подсказки по вводу адреса
/// </summary>
[Route("suggestion")]
[HttpPost]
public ApiResponseBase<ApiSuggestionResponse> Suggestion(ApiSuggestionRequest model)
{
    if (model == null) return NoDataPassed<ApiSuggestionResponse>();
    var errors = model.Validate();
    if (errors.Count > 0) return ApiResponseBase<ApiSuggestionResponse>.CreateError(ApiErrors.FieldsValidationError, errors);
    
    /// ... Логика метода
}

Сначала проверяем что модель существует, если ее нет говорим об этом клиенту. Затем получаем список ошибок на полную валидацию модели. Об идеальной структуре ответов Api можно тоже говорить долго. На сегодня я принял следующие ответы. В случае любой ошибки возвращается ее числовой код и дополнительная информация. Например, если не передана часть данных я предлагаю возвращать ответы вида:

{
  "error": {
    "code": 11,
    "errors": [
      {
        "field": "password",
        "message": "Введите пароль"
      }
    ]
  }
}

Возвращать объект error, со свойством код ошибки. В случае, если код ошибки соответствует коду об ошибки в данных тогда следует смотреть массив errors. Если же данные были проверены и ошибка уже в логике, то предлагаю возвращать такой ответ:

{
  "error": {
    "code": 20,
    "description": "Пользователь с указанным email и паролем не найден"
  }
}

Аналогично предыдущему пункту передаем объект ошибки с кодом и описанием для пользователя. Все описания следует локализовывать. Для этого в каждый клиентский запрос я передаю текущий язык пользователя. А ошибки возвращаю с сервера, таким образом я всегда могу изменить содержимое текста ошибки. 

Разобравшись с форматом ответа ошибок посмотрим как использовать эту систему. Добавив код валидации моделей в 3 серверных метода я понял, что это копипаста. Подумаем, как избавиться от нее. 

Автоматизация валидации

На помощь приходит ActionFilterAttribute. Создаем свой фильтр CustomValidatePostModelAttribute, унаследовав его от ActionFilterAttribute из пространства имен System.Net.Http.

/// <summary>
/// Атрибут валидирующий модели пост запросов
/// </summary>
public class CustomValidatePostModelAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        List<ApiFieldValidationDetails> errors = new List<ApiFieldValidationDetails>();
        
        //Если аргумент не передан или если он единственный и null
        if (!actionContext.ActionArguments.Any() ||
            (actionContext.ActionArguments.Count == 1 && actionContext.ActionArguments.First().Value == null))
        {
            actionContext.Response =
                actionContext.Request.CreateResponse(ApiResponseBase.CreateError(ApiErrors.NoDataPassed));
            return;
        }

        foreach (KeyValuePair<string, object> actionArgument in actionContext.ActionArguments)
        {
            if (actionArgument.Value is IApiFieldsValidate)
                errors.AddRange((actionArgument.Value as IApiFieldsValidate).Validate());
        }

        if (errors.Count > 0)
            actionContext.Response =
                actionContext.Request.CreateResponse(ApiResponseBase.CreateError(ApiErrors.FieldsValidationError,
                    errors));
    }
}

Данный атрибут проверяет модель запроса. В случае, если запрос пустой возвращается соответствующая ошибка. Если модель есть и она содержит интерфейс IApiFieldsValidate будет произведена валидация объекта. Если обнаружатся ошибки они будет возвращены. И запрос будет возвращен прежде, чем управление получит Action в Controller. Аналогичным образом действуют атрибуты авторизации.

Update 2016-06-21

Добавлена проверка, если Action не принимает параметров, то и валидировать нечего.

  if (actionContext.ActionDescriptor.GetParameters().Count == 0)
                return;

 

 

Комментарии

comments powered by Disqus