Search code examples
c#dependency-injectionsimple-injector

How to get service from ValidationContext using Simple Injector?


In my Asp.Net MVC Core project I use SimpleInjector as IoC. I use it because of possibility of registering open generics.

In some of my viewmodels I implement IValidatableObject.

public class MyViewmodel: IValidatableObject
{
    public string SomeProperty { get;set; }

    //...

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        //...
        IMyService service = validationContext.GetService(typeof(IMyService)) as IMyService;
    }
}

And method GetService returns null because IMyService was registered in application by SimpleInjector.

In my controller I use such a validation:

[HttpPost]
public async Task<IActionResult> Edit(MyViewmodel model)
{
    if (ModelState.IsValid)
    {
        //...
    }

    return View(model);
}

So, is there way to get IMyService from Asp.Net Core IServiceProvider in ValidationContext?


Solution

  • Although there is nothing inherently wrong with placing validation logic inside the model object itself, problems start to appear when that validation logic requires services to work. In that case you'll end up applying the Service Locator anti-pattern (by calling validationContext.GetService).

    Instead, when it comes to more complex validations that require services to run, it's much better to separate data and behavior. This allows you to move the validation logic to a separate class. This class can apply Constructor Injection and, therefore, doesn't have to use any anti-patterns.

    To achieve this, start off with your own abstraction that can validate instances. For instance:

    public interface IValidator<T>
    {
        IEnumerable<string> Validate(T instance);
    }
    

    On top of this abstraction, you can define as many implementations as you will, for instance one (or more) for validating MyViewmodel:

    public class MyViewmodelValidator : IValidator<MyViewmodel>
    {
        private readonly IMyService service;
        public MyViewmodelValidator(IMyService service) => this.service = service;
    
        public IEnumerable<string> Validate(MyViewmodel instance)
        {
            yield return "I'm not valid.";
        }
    }
    

    This is all the application code you need to get things in motion. Of course you should model the IValidator<T> interface according to your application needs.

    Only thing left is ensure MVC uses these validators when validating your view models. This can be done with a custom IModelValidatorProvider implementation:

    class SimpleInjectorModelValidatorProvider : IModelValidatorProvider
    {
        private readonly Container container;
    
        public SimpleInjectorModelValidatorProvider(Container container) =>
            this.container = container;
    
        public void CreateValidators(ModelValidatorProviderContext ctx)
        {
            var validatorType = typeof(ModelValidator<>)
                .MakeGenericType(ctx.ModelMetadata.ModelType);
            var validator =
                (IModelValidator)this.container.GetInstance(validatorType);
            ctx.Results.Add(new ValidatorItem { Validator = validator });
        }
    }
    
    // Adapter that translates calls from IModelValidator into the IValidator<T>
    // application abstraction.
    class ModelValidator<TModel> : IModelValidator
    {
        private readonly IEnumerable<IValidator<TModel>> validators;
        public ModelValidator(IEnumerable<IValidator<TModel>> validators) =>
            this.validators = validators;
    
        public IEnumerable<ModelValidationResult> Validate(
            ModelValidationContext ctx) =>
            this.Validate((TModel)ctx.Model);
    
        private IEnumerable<ModelValidationResult> Validate(TModel model) =>
            from validator in this.validators
            from errorMessage in validator.Validate(model)
            select new ModelValidationResult(string.Empty, errorMessage);
    }
    

    The only thing left to do is add SimpleInjectorModelValidatorProvider to the MVC pipeline and make the required registrations:

    services.AddMvc(options =>
        {
            options.ModelValidatorProviders.Add(
                new SimpleInjectorModelValidatorProvider(container));
        });
    
    // Register ModelValidator<TModel> adapter class
    container.Register(typeof(ModelValidator<>), typeof(ModelValidator<>),
        Lifestyle.Singleton);
    
    // Auto-register all validator implementations
    container.Collection.Register(
        typeof(IValidator<>), typeof(MyViewmodelValidator).Assembly);
    

    Et voila! There you have it—a completely loosely coupled validation structure that can be defined according to the needs of your application, while using best practices like Constructor Injection and allows your validation code to be fully tested without having to resort to anti-patterns, and without being tightly coupled with the MVC infrastructure.