How can I avoid repeated retrieval of data from the database in Fluent Validation?

67 views Asked by At

In my project I use Fluent Validation to define custom validation rules. I have noticed that in many of my validation rules, it is necessary to retrieve a value from a database to perform the validation. The problem is that I am doing the retrieval of data from the database multiple times within the various rules, and this may be impacting performance.

I was wondering if there is a way to perform data retrieval from the database once and use it in all the validation rules, so as to avoid repeated data retrieval.

In searching I found that one possible solution is to initialize the variables in the head but I'm not so sure about that

2

There are 2 answers

0
sommmen On

You could always inject a custom singleton service which could cache and share results between validators.

See: https://docs.fluentvalidation.net/en/latest/di.html

0
ajbeaven On

You can move from using individual rules for each property to using a single custom rule for the entire validated object.


Imagine we've validating a command for enrolling a student into a course. The rules for enrolling a new student in a course are:

  • Student must exist
  • Student must not already be enrolled in the course

If you were using multiple rules, you would require duplicate calls to retrieve the student from the database:

public class EnrollStudentInCourseCommandValidator : AbstractValidator<EnrollStudenInCourseCommand>
{
    public EnrollStudentInCourseCommandValidator(
        IUnitOfWork unitOfWork)
    {
        RuleFor(x => x.StudentId)
            .MustAsync(async (studentId, ct) => unitOfWork.Students.GetStudentByIdAsync(studentId) is not null)
            .WithMessage("Student with id {PropertyValue} does not exist.");

        RuleFor(x => x.CourseId)
            .MustAsync(async (command, courseId, ct) =>
            {
                var student = await unitOfWork.Students.GetStudentByIdAsync(command.StudentId);
                if (student is null)
                    return true; // handled above

                return !student.Courses.Any(c => c.Id == courseId);
            })
            .WithMessage("Student is already enrolled in course with id {PropertyValue}.");
    }
}

If instead we used a single CustomAsync rule, we could reference the same student retrieved when validating the first rule:

public class EnrollStudentInCourseCommandValidator : AbstractValidator<EnrollStudenInCourseCommand>
{
    public EnrollStudentInCourseCommandValidator(
        IUnitOfWork unitOfWork)
    {
        RuleFor(x => x)
            .CustomAsync(async (command, context, ct) =>
            {
                var student = unitOfWork.Students.GetStudentByIdAsync(studentId);
                if (student is null)
                {
                    context.AddFailure(nameof(command.StudentId), $"Student with id {command.StudentId} does not exist.");
                    return;
                }
                
                if (!student.Courses.Any(c => c.Id == courseId))
                {
                    context.AddFailure(nameof(command.CourseId), $"Student is already enrolled in course with id {command.CourseId}.");
                    return;
                }
            });
    }
}