.NET Core中结合依赖注入使用FluentValidation的实践与疑问
Great questions—let's tackle each one with practical, testable solutions aligned with .NET and FluentValidation best practices:
1. Is Your Current Approach a Best Practice?
Your move to inject IValidator<User> instead of directly instantiating UserValidation is absolutely on the right track—it decouples your service from concrete validation implementations, making unit testing trivial (you can mock IValidator to return predefined results without hitting a real database).
That said, your current DI registration has a small improvement point: manually instantiating UserValidation in the DI setup works, but FluentValidation provides official DI extensions that simplify this and handle child validators automatically (we’ll cover this in question 3). Overall, your core approach (injecting validators for testability) is a standard best practice.
2. Better Way to Handle Create/Update Scenarios Without Splitting Classes or Passing Enums
Instead of passing a DatabaseOperation enum to your validator’s constructor, use FluentValidation’s RuleSets feature—it’s designed exactly for this kind of scenario where validation rules vary by operation type. Here’s how it works:
Step 1: Define RuleSets in Your Validator
Group your create-specific and update-specific rules into named RuleSets, and keep shared rules outside of any RuleSet:
public class UserValidation : AbstractValidator<UserDTO> { private readonly IUnitOfWork _unitOfWork; public UserValidation(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; // Shared rules (apply to both create and update) RuleFor(x => x.Email).NotEmpty().EmailAddress(); // Create-specific rules RuleSet("Create", () => { RuleFor(x => x.Username).MustAsync(async (username, ct) => !await _unitOfWork.Users.ExistsAsync(username, ct)) .WithMessage("Username already exists"); }); // Update-specific rules RuleSet("Update", () => { RuleFor(x => x.Id).NotEmpty().MustAsync(async (id, ct) => await _unitOfWork.Users.ExistsAsync(id, ct)) .WithMessage("User not found"); }); // Child validator with RuleSet support RuleFor(x => x.Company) .SetValidator(new CompanyValidator(_unitOfWork)) .OverridePropertyName("Company"); } }
For child validators like CompanyValidation, you can also define RuleSets:
public class CompanyValidation : AbstractValidator<CompanyDTO> { private readonly IUnitOfWork _unitOfWork; public CompanyValidation(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; RuleSet("Create", () => { RuleFor(x => x.Name).MustAsync(async (name, ct) => !await _unitOfWork.Companies.ExistsAsync(name, ct)) .WithMessage("Company name already exists"); }); RuleSet("Update", () => { RuleFor(x => x.Id).NotEmpty().MustAsync(async (id, ct) => await _unitOfWork.Companies.ExistsAsync(id, ct)) .WithMessage("Company not found"); }); } }
Step 2: Call the Correct RuleSet in Your Service
When validating, specify which RuleSet to use based on the operation:
public class UserService { private readonly IUnitOfWork _unitOfWork; private readonly IValidator<UserDTO> _userValidator; public UserService(IUnitOfWork unitOfWork, IValidator<UserDTO> userValidator) { _unitOfWork = unitOfWork; _userValidator = userValidator; } public async Task CreateUser(UserDTO user) { // Validate using the "Create" RuleSet var validationResult = await _userValidator.ValidateAsync(user, options => options.IncludeRuleSets("Create")); if (!validationResult.IsValid) { // Handle validation errors throw new ValidationException(validationResult.Errors); } // Proceed with creation logic } public async Task UpdateUser(UserDTO user) { // Validate using the "Update" RuleSet var validationResult = await _userValidator.ValidateAsync(user, options => options.IncludeRuleSets("Update")); if (!validationResult.IsValid) { throw new ValidationException(validationResult.Errors); } // Proceed with update logic } }
This approach keeps your validator class unified, avoids passing operation enums, and still gives you full control over which rules run for each scenario.
3. Optimized DI Configuration for Validators (Including Child Validators)
FluentValidation has an official NuGet package FluentValidation.DependencyInjectionExtensions that automates validator registration and handles child validators seamlessly. Here’s how to set it up:
Step 1: Install the Package
Install-Package FluentValidation.DependencyInjectionExtensions
Step 2: Register Validators in Startup.cs (or Program.cs for .NET 6+)
Instead of manually registering each validator, use the extension method to scan your assembly and register all validators:
// For .NET Core 3.1 / Startup.cs public void ConfigureServices(IServiceCollection services) { // Register your UnitOfWork services.AddScoped<IUnitOfWork, UnitOfWork>(); // Register all validators from the assembly containing UserValidation services.AddValidatorsFromAssemblyContaining<UserValidation>(lifetime: ServiceLifetime.Scoped); // Other service registrations... services.AddControllers(); } // For .NET 6+ / Program.cs var builder = WebApplication.CreateBuilder(args); builder.Services.AddScoped<IUnitOfWork, UnitOfWork>(); builder.Services.AddValidatorsFromAssemblyContaining<UserValidation>(lifetime: ServiceLifetime.Scoped); builder.Services.AddControllers();
This will automatically register:
IValidator<UserDTO>mapped toUserValidationIValidator<CompanyDTO>mapped toCompanyValidation- All other validators in the same assembly
If you still need to pass custom parameters (though RuleSets make this unnecessary for operation types), you can use a factory approach:
// Register a factory to create validators with DatabaseOperation services.AddScoped<Func<DatabaseOperation, IValidator<UserDTO>>>(sp => op => new UserValidation(sp.GetRequiredService<IUnitOfWork>(), op));
Then inject this factory into your service:
public class UserService { private readonly IUnitOfWork _unitOfWork; private readonly Func<DatabaseOperation, IValidator<UserDTO>> _validatorFactory; public UserService(IUnitOfWork unitOfWork, Func<DatabaseOperation, IValidator<UserDTO>> validatorFactory) { _unitOfWork = unitOfWork; _validatorFactory = validatorFactory; } public async Task CreateUser(UserDTO user) { var validator = _validatorFactory(DatabaseOperation.Create); var validationResult = await validator.ValidateAsync(user); // ... } }
But again, the RuleSets approach is cleaner and more idiomatic to FluentValidation.
内容的提问来源于stack exchange,提问作者BryMan

