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?
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.
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.
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 .
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.
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.
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>();
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With