Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 948
Timothy Avatar asked Apr 24 '19 16:04

Timothy


People also ask

How do I inject a service into the validator?

There are two ways you can inject service into the validator. One is to create a wrapper service. The other option is to inject service directly into the validator. Open the gte.validator.ts.

How to retrieve services from DI container using validationcontext?

tl;dr; ValidationAttribute.IsValid () provides a ValidationContext parameter you can use to retrieve services from the DI container by calling GetService (). In ASP.NET Core MVC, as well having simple 'normal' IFilter attributes that can be used to decorate your actions, there are ServiceFilter and TypeFilter attributes.

What is the validationcontext object used for?

The context object contains a number of properties related to the object currently being validated, and also this handy number: protected override ValidationResult IsValid ( object value, ValidationContext validationContext) { var service = ( IExternalService) validationContext .

Why doesn’t simple injector integrate with [fromservices]?

While the use of [FromServices] works for services registered in ASP.NET Core’s built-in configuration system (i.e. IServiceCollection ), the Simple Injector integration package, however, does not integrate with [FromServices] out of the box. This is by design and adheres to our design guidelines, as explained below.


2 Answers

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.

like image 70
Steven Avatar answered Oct 19 '22 02:10

Steven


An amazing answer from @Steven, but for those of you wondering how to adapt it to the built-in dependency injection mechanism using IServiceProvider instead of a Container from some other library, and are stuck at

services.AddMvc(options =>
    {
        options.ModelValidatorProviders.Add(
            new SimpleInjectorModelValidatorProvider(/* TODO how do I get the IServiceProvider */));
    });

The secret sauce is to create yet another class that configures the MvcOptions and gets an IServiceProvider injected into it:

public class ConfigureMvcOptions : IConfigureOptions<MvcOptions>
{
    private readonly IServiceProvider provider;

    public ConfigureMvcOptions(IServiceProvider provider)
    {
        this.provider = provider;
    }

    public void Configure(MvcOptions options)
    {
        options.ModelValidatorProviders.Add(new SimpleInjectorModelValidatorProvider(this.provider));
    }
}

and then you can register that in Startup.cs in the usual way:

services.AddSingleton<IConfigureOptions<MvcOptions>, ConfigureMvcOptions>();
like image 30
Nicholas Piasecki Avatar answered Oct 19 '22 04:10

Nicholas Piasecki