基于父级数据的FluentValidation实现:验证服务配置速度限制
问题描述
背景
假设存在包含以下产品的数据库:
| Id | 名称 | 最大速度 |
|---|---|---|
| 40 | Lite | 100 |
| 41 | Basic | 500 |
| 42 | Premium | 1000 |
我们基于这些产品销售服务,服务可配置特定速度,且该速度不得超过关联产品的最大速度(例如订阅Basic服务时,速度配置值需在0-500之间)。
相关代码定义
C#实体
public record Product(int Id, string Name, int MaxSpeed); public record Service(string Name, int ProductId, ServiceConfiguration Configuration); public record ServiceConfiguration(int Speed, int Other, bool Options, float Whatever);
仓储接口
public interface IProductRepository { Product GetProductById(int id); }
现有验证器实现
public class ServiceValidator : AbstractValidator<Service> { public ServiceValidator(IProductRepository productRepository) { RuleFor(s => s.Name).MaximumLength(50); RuleFor(s => s.Configuration) .SetValidator(c => new ServiceConfigurationValidator(productRepository)); } }
当前ServiceValidator的构造函数接收IProductRepository但仅用于传递给ServiceConfigurationValidator,设计不够合理,需要优化。
验证需求
需要验证如下服务(JSON示例):
{ "Id": 42, "Name": "My subscription Foo Bar", "Configuration": { "Speed": 10, "Other": "Foo", "Options": "Bar" } }
需编写/改写ServiceConfigurationValidator,实现两个目标:
- 访问父级
Service对象的ProductId; - 使用该
ProductId通过IProductRepository获取对应产品,验证Configuration.Speed不超过产品的MaxSpeed。
解决方案
优化方案一:直接在ServiceValidator中实现完整验证
不需要单独为ServiceConfiguration创建独立验证器,直接在ServiceValidator中编写规则,既可以直接访问父级Service的ProductId,又避免了仓储依赖的冗余传递:
public class ServiceValidator : AbstractValidator<Service> { public ServiceValidator(IProductRepository productRepository) { // 验证服务名称长度 RuleFor(s => s.Name).MaximumLength(50); // 验证ProductId对应的产品存在 RuleFor(s => s.ProductId) .Must(id => productRepository.GetProductById(id) != null) .WithMessage("无效的ProductId:未找到对应产品"); // 验证配置速度不超过产品最大速度 RuleFor(s => s.Configuration.Speed) .Must((service, speed) => { var product = productRepository.GetProductById(service.ProductId); return speed >= 0 && speed <= product.MaxSpeed; }) .WithMessage((service, speed) => $"配置速度{speed}超过了产品ID{service.ProductId}的最大允许速度{productRepository.GetProductById(service.ProductId).MaxSpeed}"); // 可在此添加ServiceConfiguration其他属性的验证规则 RuleFor(s => s.Configuration.Other).NotEmpty(); RuleFor(s => s.Configuration.Options).NotNull(); } }
优化方案二:保留独立ServiceConfigurationValidator
如果ServiceConfiguration的验证逻辑需要复用,可通过构造函数传入父级的ProductId,明确依赖关系:
// 父级验证器 public class ServiceValidator : AbstractValidator<Service> { public ServiceValidator(IProductRepository productRepository) { RuleFor(s => s.Name).MaximumLength(50); // 验证ProductId有效性 RuleFor(s => s.ProductId) .Must(id => productRepository.GetProductById(id) != null) .WithMessage("无效的ProductId:未找到对应产品"); // 传递ProductId给子验证器 RuleFor(s => s.Configuration) .SetValidator(service => new ServiceConfigurationValidator(productRepository, service.ProductId)); } } // 子验证器 public class ServiceConfigurationValidator : AbstractValidator<ServiceConfiguration> { private readonly IProductRepository _productRepository; private readonly int _productId; public ServiceConfigurationValidator(IProductRepository productRepository, int productId) { _productRepository = productRepository; _productId = productId; // 验证速度规则 RuleFor(c => c.Speed) .Must(speed => { var product = _productRepository.GetProductById(_productId); return speed >= 0 && speed <= product.MaxSpeed; }) .WithMessage((config, speed) => $"配置速度{speed}超过了产品ID{_productId}的最大允许速度{_productRepository.GetProductById(_productId).MaxSpeed}"); // 其他配置属性验证 RuleFor(c => c.Other).NotEmpty(); RuleFor(c => c.Options).NotNull(); } }
方案说明
- 方案一逻辑更简洁,避免了子验证器的依赖传递问题,适合不需要复用配置验证逻辑的场景。
- 方案二适合
ServiceConfiguration验证逻辑需要在多个地方复用的场景,通过构造函数传入ProductId,明确了验证所需的依赖条件。 - 两种方案都添加了
ProductId有效性验证,避免因无效ID导致的空引用异常。
内容的提问来源于stack exchange,提问作者RobIII
相关产品推荐
相关产品推荐

