Skip to content

Instantly share code, notes, and snippets.

@shanji97
Created February 20, 2026 09:58
Show Gist options
  • Select an option

  • Save shanji97/34c0c9552334c77f39b94825142717a3 to your computer and use it in GitHub Desktop.

Select an option

Save shanji97/34c0c9552334c77f39b94825142717a3 to your computer and use it in GitHub Desktop.
A Fluent validation example I use in my template projects with some advanced methods, and some basic validator nesting.
using FluentValidation;
using MediaForgeProductions.Core.DTO;
using Microsoft.Extensions.Caching.Hybrid;
namespace MediaForgeProductions.Validators;
public class UserRegistrationDtoValidator : AbstractValidator<UserRegistrationDto>
{
private readonly HybridCache _cache;
public UserRegistrationDtoValidator(HybridCache cache)
{
_cache = cache;
//Full Name: letters (first charater of each space-separated part must be uppercase), spaces, hyphens, apostrophes, and periods only. Minimum length of 2 characters per word.
RuleFor(x => x.FullName)
.NotNull()
.Matches(@"^[A-Z][a-zA-Z.'-]{1,}(?: [A-Z][a-zA-Z.'-]{1,})*$")
.WithMessage("Each name part must start with an uppercase letter and be at least 2 characters long.");
RuleFor(x => x.Username)
.NotNull()
.NotEmpty()
.WithMessage("User name cannot be empty. Please provide an username.")
.WithErrorCode("400");
RuleFor(x => x.Email)
.NotEmpty()
.WithMessage("Email cannot be empty. Please provide an email address.")
.WithErrorCode("400")
.EmailAddress()
.WithMessage("Invalid email format. Please provide a valid email address.")
.WithErrorCode("400");
RuleFor(x => x.Password)
.NotNull()
.NotEmpty()
.WithMessage("Password cannot be empty. Please provide a password.")
.WithErrorCode("400")
.Matches(@"^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,}$")
.WithMessage("Password must contain at least one uppercase letter, one lowercase letter, one digit, and one special character, and be at least 8 characters long.")
.MustAsync(async (password, cancellationToken) => !await IsCommonPasswordAsync(_cache, password, cancellationToken))
.WithMessage("Password is too common. Please choose a stronger password.")
.WithErrorCode("400");
RuleFor(x => x.ConfirmPassword)
.NotNull()
.Equal(x => x.Password)
.WithMessage("Passwords do not match. Please ensure both passwords are the same.")
.WithErrorCode("400");
RuleForEach(x => x.Address)
.NotNull()
.NotEmpty()
.WithMessage("Address cannot be empty. Please provide at least one address.")
.WithErrorCode("400")
.SetValidator(new AddressDtoValidator()); // Simple properties.
RuleFor(x => x.PhoneNumber)
.NotNull()
.WithMessage("Phone number cannot be empty. Please provide a phone number.")
.WithErrorCode("400")
.ChildRules(phone =>
phone.RuleFor(p => p)
.Matches(@"^\+?[0-9]\d{1,14}$")
.WithMessage("Invalid phone number format. Please provide a valid phone number.")
.WithErrorCode("400"));
RuleFor(x => x.VATNumber)
.Null()
.DependentRules(
() => RuleFor(x => x.VATNumber)
.NotEmpty()
.WithMessage("VAT Number cannot be empty for company registrations. Please provide a VAT Number.")
.WithErrorCode("400")).When(x => x.IsCompany);
}
private static async Task<bool> IsCommonPasswordAsync(HybridCache cache, string password, CancellationToken cancellationToken)
{
const string resourceName = "MediaForgeProductions.Module.User.Resx.CommonPasswords.txt";
password = password.Trim();
if (string.IsNullOrEmpty(password))
return false;
var cachedPasswordFile = await cache.GetOrCreateAsync("common_passwords.txt", static async (CancellationToken cancellationToken) =>
{
var assembly = System.Reflection.Assembly.GetExecutingAssembly();
await using var stream = assembly.GetManifestResourceStream(resourceName)
?? throw new InvalidOperationException(
$"Embedded resource not found: {resourceName}. " +
"Ensure the file is set to 'Embedded Resource' and the namespace/path match.");
using var reader = new StreamReader(stream);
var hs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
string? line;
while ((line = await reader.ReadLineAsync(cancellationToken)) != null)
{
cancellationToken.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(line)) continue;
hs.Add(line.Trim());
}
return hs;
},
tags: ["password-file"],
cancellationToken: cancellationToken);
if (cachedPasswordFile is null || cachedPasswordFile.Count == 0)
return false;
return cachedPasswordFile.Contains(password, StringComparer.OrdinalIgnoreCase);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment