Как подключить FluentValidator к веб-API?

Я пытаюсь подключить Fluent Validation к моему проекту MVC WEB Api, и он не хочет работать.

Когда я использую MyController : Controller -> отлично работает (ModelState.IsValid возвращается False)

но когда я использую MyController :ApiController... ничего такого.

У кого-нибудь есть опыт как их подключить?

6 ответов

Решение

Последняя версия Fluent Validation (5.0.0.1) поддерживает веб-API

Просто установите его из Nuget и зарегистрируйте в Global.asax примерно так:

using FluentValidation.Mvc.WebApi;

public class WebApiApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        ...
        FluentValidationModelValidatorProvider.Configure();
    }
}

Ответ в этом запросе.

В основном вам нужно реализовать кастом ModelValidation Provider.

И еще пара вещей на заметку:

  1. Веб-API не работает с modelValidator из пространства имен System.Web.Mvc, только с теми из System.Web.Http, как отмечено здесь:

    Проверка на стороне сервера с помощью пользовательских DataAnnotationsModelValidatorProvider

  2. Вы не добавляете это так:

    ModelValidatorProviders.Providers.Add(new WebApiFluentValidationModelValidatorProvider());`
    

    А вот так:

    GlobalConfiguration.Configuration.Services.Add(typeof(System.Web.Http.Validation.ModelValidatorProvider), new WebApiFluentValidationModelValidatorProvider());`
    

Я нашел другое простое решение для использования FluentValidation в Web API, но в нем отсутствует интеграция с ModelState и Metadata. Тем не менее, при создании API, который не должен возвращать весь ModelState клиенту (как это требуется в MVC для перестройки страницы), я обнаружил, что компромисс между простотой имеет смысл. Всякий раз, когда ввод API недействителен, я возвращаю код состояния 400 Bad Request со списком идентификаторов свойств и сообщениями об ошибках. Для этого я использую простой ActionFilterAttribute:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ValidateInputsAttribute : ActionFilterAttribute
{
    private static readonly IValidatorFactory ValidatorFactory = new AttributedValidatorFactory();

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        base.OnActionExecuting(actionContext);
        var errors = new Dictionary<string, string>();
        foreach (KeyValuePair<string, object> arg in actionContext.ActionArguments.Where(a => a.Value != null))
        {
            var argType = arg.Value.GetType();
            IValidator validator = ValidatorFactory.GetValidator(argType);
            if (validator != null)
            {
                var validationResult = validator.Validate(arg.Value);
                foreach (ValidationFailure error in validationResult.Errors)
                {
                    errors[error.PropertyName] = error.ErrorMessage;
                }
            }
        }
        if (errors.Any())
        {
            actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest, errors);
        }
    }
}

Этот атрибут может быть добавлен как глобальный фильтр, к отдельным контроллерам / действиям или к базовому классу.

Этот код, безусловно, можно улучшить, но до сих пор он мне очень помог, поэтому я хотел сделать его доступным для других. Вот некоторые из его недостатков:

  1. Нулевые входы не проверены. Я думал, что это будет больше проблемой, но на практике это просто не происходит много (если вообще) в нашем приложении. Мои контроллеры генерируют ArgumentNullExceptions для нулевых входных данных, которые возвращали бы клиенту 500, сообщая клиенту, что входные данные не могут быть нулевыми.
  2. Я не могу использовать ModelState в моих контроллерах. Но после проверки того, что необходимые входные данные не равны NULL, я уже знаю, что ModelState допустим, так что это может фактически помочь упростить код. Но для разработчиков важно знать, не использовать ли это.
  3. Прямо сейчас эта реализация жестко запрограммирована для AttributedValidatorFactory. Это должно быть абстрагировано, но в моем списке приоритетов это было довольно низко.

Поскольку я пытался решить эту проблему, я хотел сделать так, чтобы один и тот же экземпляр валидатора мог использоваться для MVC и Web API. Я смог сделать это, создав две фабрики и используя их вместе.

MVC Factory:

public class MVCValidationFactory : ValidatorFactoryBase
{
    private readonly IKernel _kernel;

    public MVCValidationFactory(IKernel kernel)
    {
        _kernel = kernel;
    }

    public override IValidator CreateInstance(Type validatorType)
    {
        var returnType = _kernel.TryGet(validatorType);

        return returnType as IValidator;
    }
}

API Factory:

public class WebAPIValidationFactory : ModelValidatorProvider
{
    private readonly MVCValidationFactory _mvcValidationFactory;

    private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

    public WebAPIValidationFactory(MVCValidationFactory mvcValidationFactory)
    {
        _mvcValidationFactory = mvcValidationFactory;
    }

    public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders)
    {
        try
        {
            var type = GetType(metadata);

            if (type != null)
            {
                var fluentValidator =
                    _mvcValidationFactory.CreateInstance(typeof(FluentValidation.IValidator<>).MakeGenericType(type));

                if (fluentValidator != null)
                {
                    yield return new FluentValidationModelValidator(validatorProviders, fluentValidator);
                }
            }
        }
        catch (Exception ex)
        {
            Log.Error(ex);
        }

        return new List<ModelValidator>();
    }

    private static Type GetType(ModelMetadata metadata)
    {
        return metadata.ContainerType != null ? metadata.ContainerType.UnderlyingSystemType : null;
    }

Хитрость заключалась в том, чтобы выяснить, как выполнить проверку для MVC и веб-API. Я закончил тем, что создал оболочку для IValidator<>, которая работала с подписью ModelValidator.

public class FluentValidationModelValidator : ModelValidator
{
    public IValidator innerValidator { get; private set; }

    public FluentValidationModelValidator(
        IEnumerable<ModelValidatorProvider> validatorProviders, IValidator validator)
        : base(validatorProviders)
    {
        innerValidator = validator;
    }

    public override IEnumerable<ModelValidationResult> Validate(ModelMetadata metadata, object container)
    {
        if (InnerValidator != null && container != null)
        {
            var result = innerValidator.Validate(container);

            return GetResults(result);
        }

        return new List<ModelValidationResult>();
    }

    private static IEnumerable<ModelValidationResult> GetResults(FluentValidation.Results.ValidationResult result)
    {
        return result.Errors.Select(error =>
            new ModelValidationResult
            {
                MemberName = error.PropertyName,
                Message = error.ErrorMessage
            }));
    }
}

Последняя часть должна была подключить валидаторы в Global.asax:

MVCValidationFactory mvcValidationFactory = new MVCValidationFactory(KernelProvider.Instance.GetKernel());

GlobalConfiguration.Configuration.Services.Add(
    typeof(ModelValidatorProvider),
    new WebAPIValidationFactory(mvcValidationFactory));

ModelValidatorProviders.Providers.Add(new FluentValidationModelValidatorProvider(mvcValidationFactory));

DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;

Извините, это было немного долго, но, надеюсь, это поможет кому-то.

В WebApiConfig добавьте две строки

public static class WebApiConfig
{
   public static void Register(HttpConfiguration config)
   {
       // snip...
       //Fluent Validation
       config.Filters.Add(new ValidateModelStateFilter());
       FluentValidationModelValidatorProvider.Configure(config);
   }
}

Создайте модель и валидатор следующим образом:

[Validator(typeof(PersonCreateRequestModelValidator))] 
public class PersonCreateRequestModel
{
    public Guid PersonId { get; set; }
    public string Firstname { get; set; }
    public string Lastname { get; set; }
}


public class PersonCreateRequestModelValidator : AbstractValidator
{
    //Simple validator that checks for values in Firstname and Lastname
    public PersonCreateRequestModelValidator()
    {
        RuleFor(r => r.Firstname).NotEmpty();
        RuleFor(r => r.Lastname).NotEmpty();
    }
}

Это все, что вам нужно. Просто напишите контроллер, как вы это обычно делаете.

public IHttpActionResult Post([FromBody]PersonCreateRequestModel requestModel)
{
    //snip..
    //return Ok(some new id);
}

Если вам нужен полный пример исходного кода, вы можете получить его здесь - http://nodogmablog.bryanhogan.net/2016/12/fluent-validation-with-web-api-2/

Последняя версия Fluent Validation не поддерживает Mvc 4 или Web Api. Прочитайте это.

Другие вопросы по тегам