Dependency-Injected Validation in Web API

后端 未结 6 936
遥遥无期
遥遥无期 2021-02-04 09:05

In MVC, I can create a Model Validator which can take Dependencies. I normally use FluentValidation for this. This allows me to, for example, check on account registration tha

相关标签:
6条回答
  • 2021-02-04 09:37

    I spent a lot of time trying to find a good way around the fact that WebApi ModelValidatorProvider stores the validators as singletons. I didn't want to have to tag things with validation filters, so I ended up injecting IKernel in the validator and using that to get the context.

    public class RequestValidator : AbstractValidator<RequestViewModel>{
        public readonly IDbContext context;
    
        public RequestValidator(IKernel kernel) {
            this.context = kernel.Get<IDbContext>();
    
            RuleFor(r => r.Data).SetValidator(new DataMustHaveValidPartner(kernel)).When(r => r.RequestableKey == "join");
        }
    }
    

    This seems to work even though the validator is stored as a singleton. If you also want to be able to call it with the context, you could just create a second constructor that takes IDbContext and make the IKernel constructor pass IDbContext using kernel.Get<IDbContext>()

    0 讨论(0)
  • 2021-02-04 09:44

    I've finally got this to work, but it's a bit of a bodge. As mentioned earlier, the ModelValidatorProvider will keep Singleton instances of all Validators around, so this was completely unsuitable. Instead, I'm using a Filter to run my own validation, as suggested by Oppositional. This filter has access to the IDependencyScope and can instantiate validators neatly.

    Within the Filter, I go through the ActionArguments, and pass them through validation. The validation code was copied out of the Web API runtime source for DefaultBodyModelValidator, modified to look for the Validator within the DependencyScope.

    Finally, to make this work with the ValidationActionFilter, you need to ensure that your filters are executed in a specific order.

    I've packaged my solution up on github, with a version available on nuget.

    0 讨论(0)
  • 2021-02-04 09:46

    I was able to register and then access the Web API dependency resolver from the request using the GetDependencyScope() extension method. This allows access to the model validator when the validation filter is executing.

    Please feel free to clarify if this doesn't solve your dependency injection issues.

    Web API Configuration (using Unity as the IoC container):

    public static void Register(HttpConfiguration config)
    {
        config.DependencyResolver   = new UnityDependencyResolver(
            new UnityContainer()
            .RegisterInstance<MyContext>(new MyContext())
            .RegisterType<AccountValidator>()
    
            .RegisterType<Controllers.AccountsController>()
        );
    
        config.Routes.MapHttpRoute(
            name:           "DefaultApi",
            routeTemplate:  "api/{controller}/{id}",
            defaults:       new { id = RouteParameter.Optional }
        );
    }
    

    Validation action filter:

    public class ModelValidationFilterAttribute : ActionFilterAttribute
    {
        public ModelValidationFilterAttribute() : base()
        {
        }
    
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            var scope   = actionContext.Request.GetDependencyScope();
    
            if (scope != null)
            {
                var validator   = scope.GetService(typeof(AccountValidator)) as AccountValidator;
    
                // validate request using validator here...
            }
    
            base.OnActionExecuting(actionContext);
        }
    }
    

    Model Validator:

    public class AccountValidator : AbstractValidator<Account>
    {
        private readonly MyContext _context;
    
        public AccountValidator(MyContext context) : base()
        {
            _context = context;
        }
    
        public override ValidationResult Validate(ValidationContext<Account> context)
        {
            var result      = base.Validate(context);
            var resource    = context.InstanceToValidate;
    
            if (_context.Accounts.Any(account => String.Equals(account.EmailAddress, resource.EmailAddress)))
            {
                result.Errors.Add(
                    new ValidationFailure("EmailAddress", String.Format("An account with an email address of '{0}' already exists.", resource.EmailAddress))
                );
            }
    
            return result;
        }
    }
    

    API Controller Action Method:

    [HttpPost(), ModelValidationFilter()]
    public HttpResponseMessage Post(Account account)
    {
        var scope = this.Request.GetDependencyScope();
    
        if(scope != null)
        {
            var accountContext = scope.GetService(typeof(MyContext)) as MyContext;
            accountContext.Accounts.Add(account);
        }
    
        return this.Request.CreateResponse(HttpStatusCode.Created);
    }
    

    Model (Example):

    public class Account
    {
        public Account()
        {
        }
    
        public string FirstName
        {
            get;
            set;
        }
    
        public string LastName
        {
            get;
            set;
        }
    
        public string EmailAddress
        {
            get;
            set;
        }
    }
    
    public class MyContext
    {
        public MyContext()
        {
        }
    
        public List<Account> Accounts
        {
            get
            {
                return _accounts;
            }
        }
        private readonly List<Account> _accounts = new List<Account>();
    }
    
    0 讨论(0)
  • FluentValidation has had support for WebApi for quite sometime (not sure if your question dates before that): https://fluentvalidation.codeplex.com/discussions/533373

    Quoting from the thread:

    {
       GlobalConfiguration.Configuration.Services.Add(typeof(System.Web.Http.Validation.ModelValidatorProvider),
           new WebApiFluentValidationModelValidatorProvider()
           {
               AddImplicitRequiredValidator = false //we need this otherwise it invalidates all not passed fields(through json). btw do it if you need
           });
           FluentValidation.ValidatorOptions.ResourceProviderType = typeof(FluentValidationMessages); // if you have any related resource file (resx)
           FluentValidation.ValidatorOptions.CascadeMode = FluentValidation.CascadeMode.Continue; //if you need!
    

    I have been using it in WebApi2 project without any issues.

    0 讨论(0)
  • 2021-02-04 09:49

    This certainly isn't recommended as the class is internal, but you can remove the IModelValidatorCache services in your WebApi config.

    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            config.Services.Clear(Type.GetType("System.Web.Http.Validation.IModelValidatorCache, System.Web.Http"));
        }
    }
    
    0 讨论(0)
  • 2021-02-04 10:00

    I have DI working with Fluent Validators in WebApi no problems. I've found that the validators get called a lot, and these sort of heavy logic validations have no place in a model validator. Model validators, in my opinion, are meant to be lightweight checking the shape of the data. Does Email look like an email and has the caller provided FirstName, LastName and either Mobile or HomePhone?

    Logic validation like Can this email be registered belongs in the service layer, not at a controller. My implementations also don't share an implicit data context since I think that's an anti-pattern.

    I think the current NuGet package for this has an MVC3 dependency, so I ended up just looking at the source directly and creating my own NinjectFluentValidatorFactory.

    In App_Start/NinjectWebCommon.cs we have the following.

        /// <summary>
        /// Set up Fluent Validation for WebApi.
        /// </summary>
        private static void FluentValidationSetup(IKernel kernel)
        {
            var ninjectValidatorFactory
                            = new NinjectFluentValidatorFactory(kernel);
    
            // Configure MVC
            FluentValidation.Mvc.FluentValidationModelValidatorProvider.Configure(
                provider => provider.ValidatorFactory = ninjectValidatorFactory);
    
            // Configure WebApi
            FluentValidation.WebApi.FluentValidationModelValidatorProvider.Configure(
                System.Web.Http.GlobalConfiguration.Configuration,
                provider => provider.ValidatorFactory = ninjectValidatorFactory);
    
            DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;
        }
    

    I believe the only other required packages for the above are:

      <package id="FluentValidation" version="5.1.0.0" targetFramework="net451" />
      <package id="FluentValidation.MVC5" version="5.1.0.0" targetFramework="net451" />
      <package id="FluentValidation.WebApi" version="5.1.0.0" targetFramework="net451" />
      <package id="Ninject" version="3.2.0.0" targetFramework="net451" />
      <package id="Ninject.MVC3" version="3.2.0.0" targetFramework="net451" />
      <package id="Ninject.Web.Common" version="3.2.0.0" targetFramework="net451" />
    
    0 讨论(0)
提交回复
热议问题