Created
February 20, 2026 09:58
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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