Как подключить 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.
И еще пара вещей на заметку:
Веб-API не работает с modelValidator из пространства имен System.Web.Mvc, только с теми из System.Web.Http, как отмечено здесь:
Проверка на стороне сервера с помощью пользовательских DataAnnotationsModelValidatorProvider
Вы не добавляете это так:
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);
}
}
}
Этот атрибут может быть добавлен как глобальный фильтр, к отдельным контроллерам / действиям или к базовому классу.
Этот код, безусловно, можно улучшить, но до сих пор он мне очень помог, поэтому я хотел сделать его доступным для других. Вот некоторые из его недостатков:
- Нулевые входы не проверены. Я думал, что это будет больше проблемой, но на практике это просто не происходит много (если вообще) в нашем приложении. Мои контроллеры генерируют ArgumentNullExceptions для нулевых входных данных, которые возвращали бы клиенту 500, сообщая клиенту, что входные данные не могут быть нулевыми.
- Я не могу использовать ModelState в моих контроллерах. Но после проверки того, что необходимые входные данные не равны NULL, я уже знаю, что ModelState допустим, так что это может фактически помочь упростить код. Но для разработчиков важно знать, не использовать ли это.
- Прямо сейчас эта реализация жестко запрограммирована для 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. Прочитайте это.