Reputation: 3105
For example, you may validate a phone number or email, those are all over FV documentation, but what if you need to make sure the phone number is not already in use by an employee of manager "X"? I know its possible to do this through all the custom validator options. But should I be doing it there?
If I put this logic into a complex custom FluentValidation thingy I worry that I am coding in this logic in the wrong place. Traditionally, I would just add this to my service in the areas where I would normally be doing a add/save.
Since I already have fluent validation set up for my view model, and I am doing other types of validations there, it seems enticing to keep everything in one place. However, this means if I ever reuse my logic in a non web app ill have to remember to execute these validators some other way instead of this being automated via the modelbinder IsValid()
. Although I did not spend the extra time coding to make my project this modular anyway. I digress.
Upvotes: 6
Views: 2011
Reputation: 2909
When using FluentValidation
for web request validation, it's important to understand where to draw the line between simple validations and those that require database access. You should not blend the two. Validation is not data verification. Validation is simply checking an object for validity, prior to any database access or data interaction. There is little sense in database access prior to validating a request object! Think about it.
FluentValidation
Basic Validations: Use FluentValidation
for validating properties that can be checked without database access. This includes:
Uniqueness Checks: When you need to ensure a value is unique (e.g., username, email), you'll likely need to query the database.
Existence Checks: When validating that a related entity exists (e.g., foreign key relationships), a database query is necessary.
Complex Business Rules: Some business rules may require querying the database to ensure they are met.
To separate these concerns cleanly, use FluentValidation for basic checks and perform database checks within your service layer or a custom validator.
You can create a custom validator that includes database checks
public class UniqueEmailValidator : AbstractValidator<UserModel>
{
private readonly IUserRepository _userRepository;
public UniqueEmailValidator(IUserRepository userRepository)
{
_userRepository = userRepository;
RuleFor(x => x.Email)
.NotEmpty()
.EmailAddress()
.MustAsync(BeUniqueEmail).WithMessage("Email already exists.");
}
private async Task<bool> BeUniqueEmail(string email, CancellationToken cancellationToken)
{
return !(await _userRepository.EmailExistsAsync(email, cancellationToken));
}
}
Ensure your custom validator is registered with the dependency injection container:
services.AddTransient<IValidator<UserModel>, UniqueEmailValidator>();
For more complex validation logic, consider handling these checks in your service layer
public class UserService
{
private readonly IUserRepository _userRepository;
private readonly IValidator<UserModel> _validator;
public UserService(IUserRepository userRepository, IValidator<UserModel> validator)
{
_userRepository = userRepository;
_validator = validator;
}
public async Task<Result> CreateUserAsync(UserModel model)
{
var validationResult = await _validator.ValidateAsync(model);
if (!validationResult.IsValid)
{
return Result.Failure(validationResult.Errors);
}
// Perform database validations
if (await _userRepository.EmailExistsAsync(model.Email))
{
return Result.Failure("Email already exists.");
}
// Proceed with user creation
await _userRepository.AddUserAsync(model);
return Result.Success();
}
}
You can maintain a clean separation of concerns, ensuring that your validations are appropriately handled both in-memory and with database access where necessary.
Upvotes: 2
Reputation: 9815
Well no it is actually a good place. You will want to keep the validation at the same place and make it reusable for your logic, no matter the underlying framework.
Using attribute-validation and ModelStateDictionary in ASP.NET bounds your logic to ASP.NET while FluentValidation can be used with everything. You could still use a service to wrap that logic and only add / update entities there when validation succeeds. I heavily use fluentvalidation, even for scenarios where I need to do database-queries and it works just fine. I could simply switch from ASP.NET to a console application or whatever, and have the very same logic for both.
Having validation logic splitted into several areas sounds not like best practice.
Upvotes: 9