web-dev-qa-db-ja.com

FluentValidatorをWeb APIにフックする方法は?

Fluent ValidationをMVC WEB Apiプロジェクトにフックしようとしていますが、機能しません。

MyController : Controllerを使用すると、正常に機能します(ModelState.IsValidFalseを返します)

しかし、MyController :ApiControllerを使用すると...何も起こりません。

誰かがそれらを接続する方法の経験がありますか?

30
Marty

fluent Validation(5.0.0.1)の最新バージョンはWeb APIをサポートしています

Nugetからインストールして、Global.asaxに次のように登録するだけです。

using FluentValidation.Mvc.WebApi;

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

答えはこの pull request にあります。

基本的に、カスタムModelValidationプロバイダーを実装する必要があります。

さらに、注意すべきことがいくつかあります。

  1. Web APIは、System.Web.Mvc名前空間のmodelValidatorでは機能せず、System.Web.Httpからのものは次のように機能します。

    カスタムDataAnnotationsModelValidatorProviderによるサーバー側の検証

  2. 次のように追加しないでください。

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

    しかし、このように:

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

Web APIでFluentValidationを使用するための別の簡単な解決策を見つけましたが、ModelStateおよびMetadataとの統合が欠如しています。ただし、ModelState全体をクライアントに返す必要のないAPIを構築する場合(MVCでページを再構築するために必要)、シンプルさのトレードオフに価値があることがわかりました。 API入力が無効な場合は常に、プロパティIDとエラーメッセージのリストを含む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. Null入力は検証されません。これはもっと問題になるだろうと思っていましたが、実際には、私たちのアプリでは(たとえあったとしても)多くは起こりません。私のコントローラーは、null入力に対してArgumentNullExceptionsをスローします。これにより、500がクライアントに返され、入力をnullにできないことをクライアントに通知します。
  2. コントローラーでModelStateを使用できません。しかし、必要な入力がnullでないことを検証した後、ModelStateが有効であることをすでに知っているので、これは実際にコードを簡略化するのに役立つ場合があります。しかし、開発者がそれを使用しないことを知ることは重要です。
  3. 現在、この実装はAttributedValidatorFactoryに対してハードコードされています。これは抽象化する必要がありますが、これまでのところ私の優先度リストではかなり低くなっています。
3
Matt Scully

私はこれを解決しようとしていたので、MVCとWeb APIに同じバリデーターインスタンスを使用できるようにしたかったのです。私は2つの工場を作り、それらを一緒に使用することによってこれを達成することができました。

MVCファクトリー:

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ファクトリ:

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とWeb APIの両方の検証を実行する方法を理解することでした。最終的に、ModelValidatorシグネチャで動作するIValidator <>のラッパーを作成しました。

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;

申し訳ありませんがこれは少し長かったですが、うまくいけば誰かを助けるのに役立ちます。

3
Joel

WebApiConfigに2行を追加します

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/

2
Bryan