Modern Model Validation in ASP.NET Core 8 with FluentValidation and BaseValidator

Modern Model Validation in ASP.NET Core 8 with FluentValidation and BaseValidator

Validation is a crucial part of any web application. It ensures that the data entering your system is correct, consistent, and secure. While ASP.NET Core provides built-in model validation, FluentValidation offers a more flexible, readable, and maintainable approach.

In this blog, we’ll explore how to implement a centralized validation system using a BaseValidator, making your code DRY, clean, and scalable.

Why FluentValidation?

FluentValidation allows you to:

  • Define rules in a fluent, readable style.

  • Separate validation logic from controllers and models.

  • Reuse validation logic across multiple models.

  • Easily integrate with ASP.NET Core.

Instead of repeating the same checks across multiple models, FluentValidation keeps your code consistent and maintainable.

using FluentValidation;

 

public abstract class BaseValidator<T> : AbstractValidator<T>
{
    // Validate a string (required + length)
    protected IRuleBuilderOptions<T, string> ValidString(
        IRuleBuilder<T, string> rule, int minLength = 2, int maxLength = 100)
    {
        return rule
            .NotEmpty().WithMessage("{PropertyName} is required.")
            .Length(minLength, maxLength).WithMessage("{PropertyName} must be between {MinLength} and {MaxLength} characters.");
    }

 

    // Validate an email
    protected IRuleBuilderOptions<T, string> ValidEmail(IRuleBuilder<T, string> rule)
    {
        return rule
            .NotEmpty().WithMessage("Email is required.")
            .EmailAddress().WithMessage("Please enter a valid email address.");
    }

 

    // Validate a password (required + length + complexity)
    protected IRuleBuilderOptions<T, string> ValidPassword(
        IRuleBuilder<T, string> rule, int minLength = 8, int maxLength = 100)
    {
        return rule
            .NotEmpty().WithMessage("Password is required.")
            .Length(minLength, maxLength)
                .WithMessage($"Password must be between {minLength} and {maxLength} characters.")
            .Matches("[A-Z]").WithMessage("Password must contain at least one uppercase letter.")
            .Matches("[0-9]").WithMessage("Password must contain at least one number.");
    }
}

✅ Benefits

  • Centralized validation rules.

  • Easy to update defaults (e.g., change minimum password length).

  • Cleaner and shorter derived validators.

Step 2: Create a Validator for Your Model

Here’s how you can create a RegisterUserValidator using the BaseValidator:

public class RegisterUserValidator : BaseValidator<RegisterUserModel>
{
    public RegisterUserValidator()
    {
        ValidString(RuleFor(x => x.FullName));               // default 2–100
        ValidPassword(RuleFor(x => x.Password), 6, 50);     // custom min/max + complexity
        ValidEmail(RuleFor(x => x.Email));                  // email validation

 

        RuleFor(x => x.LanguageId)
            .GreaterThan(0)
            .WithMessage("Please select a valid language.");

 

        RuleFor(x => x.RegionId)
            .GreaterThan(0)
            .WithMessage("Please select a valid region.");
    }
}

Notice how the validator is much cleaner. All repetitive rules like string length, email format, and password complexity are handled by the BaseValidator.

Step 3: Automatic Validation with an Action Filter

To avoid manually calling validators in each controller, you can use an Action Filter:

public class ValidationFilter(IServiceProvider serviceProvider) : IAsyncActionFilter
{
    private readonly IServiceProvider _serviceProvider = serviceProvider;

 

    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        foreach (object? argument in context.ActionArguments.Values)
        {
            if (argument == null) continue;

 

            var validatorType = typeof(IValidator<>).MakeGenericType(argument.GetType());
            IValidator? validator = _serviceProvider.GetService(validatorType) as IValidator;

 

            if (validator != null)
            {
                var validationResult = await validator.ValidateAsync(new ValidationContext<object>(argument));

 

                if (!validationResult.IsValid)
                {
                    context.Result = new BadRequestObjectResult(new
                    {
                        success = false,
                        errors = validationResult.Errors.Select(e => new
                        {
                            field = e.PropertyName,
                            message = e.ErrorMessage
                        })
                    });
                    return;
                }
            }
        }

 

        await next();
    }
}

Register the filter and validators in Program.cs:

builder.Services.AddControllers(options => options.Filters.Add<ValidationFilter>());
builder.Services.AddValidatorsFromAssemblyContaining<RegisterUserValidator>();

✅ This automatically validates all incoming models and returns a consistent JSON error response.

Step 4: Benefits of This Setup

  1. DRY & Consistent: Reusable rules in BaseValidator.

  2. Cleaner Validators: Focus only on model-specific rules.

  3. Centralized Maintenance: Change password rules or string lengths in one place.

  4. Automatic Validation: Action filter handles all validation with a standard error response.

  5. Flexible & Customizable: Override defaults per property if needed.


Conclusion

Using a BaseValidator in combination with FluentValidation and a custom Action Filter gives you a modern, maintainable, and scalable validation system in ASP.NET Core 8.

  • Centralize common rules like strings, emails, and passwords.

  • Keep your validators short and readable.

  • Automatically validate all incoming models and provide consistent error responses.

This approach is ideal for medium to large projects where you have multiple models and want to enforce consistent validation rules across your application.


Thanks, for reading the blog, I hope it helps you. Please share this link on your social media accounts so that others can read our valuable content. Share your queries with our expert team and get Free Expert Advice for Your Business today.


About Writer

Ravinder Singh

Full Stack Developer
I have 15+ years of experience in commercial software development. I write this blog as a kind of knowledge base for myself. When I read about something interesting or learn anything I will write about it. I think when writing about a topic you concentrate more and therefore have better study results. The second reason why I write this blog is, that I love teaching and I hope that people find their way on here and can benefit from my content.

Hire me on Linkedin

My portfolio

Ravinder Singh Full Stack Developer