Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dependency-Injected Validation in Web API

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 that an e-mail address hasn't been used (NB: This is a simplified example!):

public class RegisterModelValidator : AbstractValidator<RegisterModel> {
    private readonly MyContext _context;
    public RegisterModelValidator(MyContext context) {
        _context = context;
    }
    public override ValidationResult Validate(ValidationContext<RegisterModel> context) {
        var result = base.Validate(context);
        if (context.Accounts.Any(acc => acc.Email == context.InstanceToValidate.Email)){
            result.Errors.Add(new ValidationFailure("Email", "Email has been used"));
        }
        return result;
    }
}

No such integration exists for Web API with FluentValidation. There have been a couple of attempts at this, but neither have tackled the Dependency Injection aspect and only work with static validators.

The reason this is difficult is due to the different in implementation of ModelValidatorProvider and ModelValidator between MVC and Web API. In MVC, these are instantiated per-request (hence injecting a context is easy). In Web API, they are static, and the ModelValidatorProvider maintains a cache of ModelValidators per type, to avoid unnecessary reflection lookups on every request.

I've been trying to add the necessary integration myself, but have been stuck trying to obtain the Dependency Scope. Instead, I thought I'd step back and ask if there any other solutions to the problem - if anyone has come up with a solution to performing Model Validation where dependencies can be injected.

I do NOT want to perform the validation within the Controller (I am using a ValidationActionFilter to keep this separate), which means I can't get any help from the constructor injection of the controller.

like image 521
Richard Avatar asked Feb 26 '13 15:02

Richard


People also ask

What is dependency injection in Web API?

“ - In software development, dependency injection is a technique where one object supplies the needs, or dependencies, of another object.

What kind of validation is used in Web API?

Web API does not automatically return an error to the client when validation fails. It is up to the controller action to check the model state and respond appropriately. If model validation fails, this filter returns an HTTP response that contains the validation errors.

What is dependency injection in net core with example?

ASP.NET Core supports the dependency injection (DI) software design pattern, which is a technique for achieving Inversion of Control (IoC) between classes and their dependencies. For more information specific to dependency injection within MVC controllers, see Dependency injection into controllers in ASP.NET Core.


2 Answers

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>();
}
like image 100
Oppositional Avatar answered Oct 21 '22 10:10

Oppositional


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.

like image 4
Richard Avatar answered Oct 21 '22 10:10

Richard