Assume I have a database with products:
Id | Name | MaxSpeed |
---|---|---|
40 | Lite | 100 |
41 | Basic | 500 |
42 | Premium | 1000 |
And assume we sell services based on these products. These services can be configured for a specific speed up to the associated product's maximum speed. So when I subscribe to a basic service, I am allowed to put any values between 0 and 500 for my speed configuration.
public record Product(int Id, string Name, int MaxSpeed);
public record Service(string Name, int ProductId, ServiceConfiguration Configuration);
public record ServiceConfiguration(int Speed, int Other, bool Options, float Whatever);
I also have a repository class to provide access to my database:
public interface IProductRepository
{
Product GetProductById(int id);
}
So now I want to implement validation. I start with a ServiceValidator
:
public class ServiceValidator : AbstractValidator<Service>
{
public ServiceValidator(IProductRepository productRepository)
{
RuleFor(s => s.Name).MaximumLength(50);
RuleFor(s => s.Configuration)
.SetValidator(c => new ServiceConfigurationValidator(productRepository));
}
}
I have a constructor here that takes an IProductRepository
, which is kinda weird since it doesn't really use it, it just needs to pass it on the the ServiceConfigurationValidator
. Suggestions on how to improve this are welcome.
Then on to my actual question: Let's say I want to validate the following service:
{
"Id": 42,
"Name": "My subscription Foo Bar",
"Configuration": {
"Speed": 10,
"Other": "Foo",
"Options": "Bar"
}
}
So I start implementing a ServiceConfigurationValidator
:
public class ServiceConfigurationValidator : AbstractValidator<ServiceConfiguration>
{
public ServiceConfigurationValidator(IProductRepository productRepository)
{
// Use database to retrieve product 42, get it's associated max. speed and ensure the
// serviceconfiguration value is equal to, or less than, the products max. speed.
}
}
How would I (re)write this ServiceConfigurationValidator
so that:
IProductRepository
and check that the Configuration.Speed
doesn't exceed the product's MaxSpeed
?I've looked into PropertyValidators
, gone through Fluentvalidation's documentation but I can't for the life of me find anything that points me in the correct direction.
Writing a Custom Validator is what you need.
Approach 1: With .Must()
RuleFor(s => s.Configuration)
.Must((parent, config, context) =>
{
Product product = productRepository.GetProductById(parent.ProductId);
if (product == null)
{
// Handle error for product not existed (if needed)
context.AddFailure("Product is not existed!");
return false;
}
context.MessageFormatter.AppendArgument("MaxSpeed", product.MaxSpeed);
return config.Speed <= product.MaxSpeed;
})
.WithMessage("{PropertyName} must be lesser than or equal to {MaxSpeed}.");
Approach 2: With .Custom()
RuleFor(s => s.Configuration)
.Custom((config, context) =>
{
Product product = productRepository.GetProductById(context.InstanceToValidate.ProductId);
if (product == null)
{
// Handle error for product not existed (if needed)
context.AddFailure("Product is not existed!");
return;
}
if (config.Speed > product.MaxSpeed)
{
context.MessageFormatter.AppendArgument("PropertyName", nameof(config.Speed));
context.MessageFormatter.AppendArgument("MaxSpeed", product.MaxSpeed);
context.AddFailure("{PropertyName} must be lesser than or equal to {MaxSpeed}.");
return;
}
});
Demo (Approach 1 & 2) @ .NET Fiddle
I don't think that you need to build the custom validator with the PropertyValidator
abstract class as it is designed for complexity and generic purpose. The above approaches should be enough for your use case.
If you are still keen to implement a custom validator in a separate class, your custom validator class have to inherit the PropertyValidator<T, TElement>
abstract class.
Approach 3: With PropertyValidator<T, TElement>
RuleFor(s => s.Configuration)
.SetValidator(new ServiceConfigurationValidator(productRepository));
public class ServiceConfigurationValidator : PropertyValidator<Service, ServiceConfiguration>
{
private readonly IProductRepository _productRepository;
public ServiceConfigurationValidator(IProductRepository productRepository)
{
_productRepository = productRepository; ;
}
public override bool IsValid(ValidationContext<Service> context, ServiceConfiguration value)
{
Product product = _productRepository.GetProductById(context.InstanceToValidate.ProductId);
if (product == null)
{
// Handle error for product not existed (if needed)
context.AddFailure("Product is not existed!");
return false;
}
if (value.Speed > product.MaxSpeed)
{
context.MessageFormatter.AppendArgument("PropertyName", nameof(value.Speed));
context.MessageFormatter.AppendArgument("MaxSpeed", product.MaxSpeed);
context.AddFailure("{PropertyName} must be lesser than or equal to {MaxSpeed}.");
return false;
}
return true;
}
public override string Name => "ServiceConfigurationValidator";
}