For my .Net 5 workerservice app I want to validate options by implementing the IValidateOptions
interface but don't want to write my own error messages. That's why I want to make use of the package FluentValidation.AspNetCore.
Given the model
namespace App.Models
{
public class MyOptions
{
public string Text { get; set; }
}
}
I added validation rules
namespace App.ModelValidators
{
public class MyOptionsValidator : AbstractValidator<MyOptions>
{
public MyOptionsValidator()
{
RuleFor(model => model.Text).NotEmpty();
}
}
}
Next I'm using this validator inside the validation
namespace App.OptionsValidators
{
public class MyOptionsValidator : IValidateOptions<MyOptions>
{
private readonly IValidator<MyOptions> _validator;
public MyOptionsValidator(IValidator<MyOptions> validator)
{
_validator = validator;
}
public ValidateOptionsResult Validate(string name, MyOptions options)
{
var validationResult = _validator.Validate(options);
if (validationResult.IsValid)
{
return ValidateOptionsResult.Success;
}
return ValidateOptionsResult.Fail(validationResult.Errors.Select(validationFailure => validationFailure.ErrorMessage));
}
}
}
Lastly I setup the DI container
services.AddTransient<IValidator<MyOptions>, ModelValidators.MyOptionsValidator>();
services.AddSingleton<IValidateOptions<MyOptions>, OptionsValidators.MyOptionsValidator>();
services.Configure<MyOptions>(configuration.GetSection("My"));
I would like to know if this can be simplified?
Maybe I can just implement the IValidateOptions
interface, avoid the AbstractValidator
and write fluent rules inside the .Validate()
method?
Sample code what I want to achieve
namespace App.OptionsValidators
{
public class MyOptionsValidator : IValidateOptions<MyOptions>
{
public ValidateOptionsResult Validate(string name, MyOptions options)
{
var validationResult = options.Text.Should.Not.Be.Empty();
if (validationResult.IsValid)
{
return ValidateOptionsResult.Success;
}
return ValidateOptionsResult.Fail(validationResult.ErrorMessage);
}
}
}
so I don't need the AbstractValidator<MyOptions>
anymore.
My first approach I'm not sure about
Instead of using FluentValidation I'm using DataAnnotations.
[Required]
attribute to the Text
propertyMyOptionsValidator : AbstractValidator<MyOptions>
.
services.AddSingleton<IValidateOptions<MyOptions>, OptionsValidators.MyOptionsValidator>();
services.Configure<MyOptions>(configuration.GetSection("My"));
Inside MyOptionsValidator
I validate the options like so
public ValidateOptionsResult Validate(string name, MyOptions options)
{
var validationResults = new List<ValidationResult>();
if (!Validator.TryValidateObject(options, new ValidationContext(options), validationResults, true))
{
return ValidateOptionsResult.Fail(validationResults.Select(validationResult => validationResult.ErrorMessage));
}
return ValidateOptionsResult.Success;
}
but maybe there are still better ways :)
I have a strong preference to utilise the same approach for validation across the stack, and in my case this is via FluentValidation. The following would be my approach.
Create a new base validator for your options/settings validators:
public abstract class AbstractOptionsValidator<T> : AbstractValidator<T>, IValidateOptions<T>
where T : class
{
public virtual ValidateOptionsResult Validate(string name, T options)
{
var validateResult = this.Validate(options);
return validateResult.IsValid ? ValidateOptionsResult.Success : ValidateOptionsResult.Fail(validateResult.Errors.Select(x => x.ErrorMessage));
}
}
This extends the FluentValidation AbstractValidator<T>
to support IValidateOptions<T>
. You've now got a base that you can use for all of your options/settings validators. For the following settings:
public class FooSettings
{
public string Bar { get; set; }
}
you end up with a typical validator:
public class FooSettingsValidator : AbstractOptionsValidator<FooSettings>, IFooSettingsValidator
{
public FooSettingsValidator()
{
RuleFor(x => x.Bar).NotEmpty();
}
}
Let the DI container know about it:
serviceCollection.AddSingleton<IValidateOptions<FooSettings>, FooSettingsValidator>();
If there is nothing built in to do the above, I'd look to Scrutor to turn this into an automatic process.
All of the above affords me all of the benefits of using FluentValidation, whilst utilising the first class options validation support that Microsoft has provided for us.
LINQPad working example:
using static FluentAssertions.FluentActions;
void Main()
{
var fixture = new Fixture();
var validator = new FooSettingsValidator();
validator.Validate(fixture.Build<FooSettings>().Without(x => x.Bar).Create()).Errors.Select(x => x.ErrorMessage).Should().BeEquivalentTo(new string[] { "'Bar' must not be empty." });
validator.Validate(fixture.Build<FooSettings>().Create()).Errors.Select(x => x.ErrorMessage).Should().BeEquivalentTo(new string[] { });
using (var scope = ServiceProvider.Create(bar: null).CreateScope())
{
Invoking(() => scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<FooSettings>>().Value).Should().Throw<OptionsValidationException>();
}
using (var scope = ServiceProvider.Create(bar: "asdf").CreateScope())
{
scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<FooSettings>>().Value.Bar.Should().Be("asdf");
}
}
// You can define other methods, fields, classes and namespaces here
public class FooSettings
{
public string Bar { get; set; }
}
public interface IFooSettingsValidator : IValidator { }
public class FooSettingsValidator : AbstractOptionsValidator<FooSettings>, IFooSettingsValidator
{
public FooSettingsValidator()
{
RuleFor(x => x.Bar).NotEmpty();
}
}
public abstract class AbstractOptionsValidator<T> : AbstractValidator<T>, IValidateOptions<T>
where T : class
{
public virtual ValidateOptionsResult Validate(string name, T options)
{
var validateResult = this.Validate(options);
return validateResult.IsValid ? ValidateOptionsResult.Success : ValidateOptionsResult.Fail(validateResult.Errors.Select(x => x.ErrorMessage));
}
}
public class ServiceProvider
{
public static IServiceProvider Create(string bar)
{
var serviceCollection = new ServiceCollection();
var config = new ConfigurationBuilder()
.AddInMemoryCollection(
new List<KeyValuePair<string, string>> { new KeyValuePair<string, string>("Foo:Bar", bar) })
.Build();
serviceCollection.AddSingleton<IConfiguration>(config);
serviceCollection.AddOptions();
//serviceCollection.Configure<FooSettings>(config.GetSection("Foo"));
serviceCollection.AddOptions<FooSettings>()
.Bind(config.GetSection("Foo"));
serviceCollection.AddSingleton<IValidateOptions<FooSettings>, FooSettingsValidator>();
serviceCollection.AddSingleton<IFooSettingsValidator, FooSettingsValidator>();
return serviceCollection.BuildServiceProvider();
}
}