How to get service from ValidationContext using Simple Injector?

Deadly 提交于 2020-01-23 11:07:06

问题


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?


回答1:


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.



来源:https://stackoverflow.com/questions/55835059/how-to-get-service-from-validationcontext-using-simple-injector

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!