ASP.NET MVC视图模型用ExpressiveAnnotations验证是否为不良实践?
问题分析与优化方案
原实现属于不良实践,原因如下:
- 违反单一职责原则:视图模型(
SampleViewModel)的核心职责是封装视图层的数据输入与展示,不应包含数据库查询这类业务/数据访问逻辑,导致ViewModel职责混杂,难以维护和测试。 - 重复数据库查询:
PropertyA和PropertyB各自标注[AssertThat]验证,每次验证都会触发IsCompliant属性的getter,也就是两次重复的数据库请求,造成不必要的性能损耗。 - 验证逻辑耦合度高:合规性验证逻辑与ViewModel绑定,无法在其他场景复用;后续修改验证规则需直接改动ViewModel,不符合开闭原则。
- 客户端验证失效:ExpressiveAnnotations支持客户端验证,但
IsCompliant中的数据库查询逻辑无法在客户端执行,会导致客户端与服务端验证不一致,降低验证逻辑的可靠性和用户体验。
更优实现方案
方案1:使用自定义验证属性分离业务逻辑
将合规性验证逻辑封装到自定义ValidationAttribute中,依赖业务服务完成数据库查询,ViewModel仅负责传递数据:
using System.ComponentModel.DataAnnotations; // 自定义验证属性 public class CompliantValidationAttribute : ValidationAttribute { private readonly string _propertyAName; private readonly string _propertyBName; public CompliantValidationAttribute(string propertyAName, string propertyBName) { _propertyAName = propertyAName; _propertyBName = propertyBName; } protected override ValidationResult IsValid(object value, ValidationContext validationContext) { // 通过ValidationContext获取依赖注入的业务服务 var queryRepository = validationContext.GetService(typeof(SampleQueryRepository)) as SampleQueryRepository; if (queryRepository == null) { return new ValidationResult("无法获取验证所需的服务"); } // 获取PropertyA和PropertyB的当前值 var propertyAValue = validationContext.ObjectType.GetProperty(_propertyAName)?.GetValue(validationContext.ObjectInstance) as string; var propertyBValue = validationContext.ObjectType.GetProperty(_propertyBName)?.GetValue(validationContext.ObjectInstance) as string; if (!queryRepository.IsCompliant(propertyAValue, propertyBValue)) { return new ValidationResult(ErrorMessage ?? "Not compliant."); } return ValidationResult.Success; } } // 视图模型 public class SampleViewModel { [CompliantValidation(nameof(PropertyA), nameof(PropertyB), ErrorMessage = "Not compliant.")] public string PropertyA { get; set; } [CompliantValidation(nameof(PropertyA), nameof(PropertyB), ErrorMessage = "Not compliant.")] public string PropertyB { get; set; } }
方案2:将复杂验证移至业务服务层
ViewModel仅保留基础输入验证(如格式、必填),复杂业务验证(需数据库查询)放在业务服务中处理,避免ViewModel依赖数据访问逻辑:
// 视图模型(仅基础验证) public class SampleViewModel { [Required] public string PropertyA { get; set; } [Required] public string PropertyB { get; set; } } // 业务服务 public class SampleService { private readonly SampleQueryRepository _queryRepository; public SampleService(SampleQueryRepository queryRepository) { _queryRepository = queryRepository; } public bool IsViewModelCompliant(SampleViewModel model) { return _queryRepository.IsCompliant(model.PropertyA, model.PropertyB); } } // 控制器中使用 public class SampleController : Controller { private readonly SampleService _sampleService; public SampleController(SampleService sampleService) { _sampleService = sampleService; } [HttpPost] public IActionResult Submit(SampleViewModel model) { if (!ModelState.IsValid) { return View(model); } if (!_sampleService.IsViewModelCompliant(model)) { ModelState.AddModelError(string.Empty, "Not compliant."); return View(model); } // 执行后续业务逻辑 return RedirectToAction("Success"); } }
方案3:结合FluentValidation(推荐)
使用FluentValidation库替代DataAnnotations,更灵活地组织验证逻辑,完全分离ViewModel与验证规则:
using FluentValidation; // 视图模型 public class SampleViewModel { public string PropertyA { get; set; } public string PropertyB { get; set; } } // 验证器 public class SampleViewModelValidator : AbstractValidator<SampleViewModel> { private readonly SampleQueryRepository _queryRepository; public SampleViewModelValidator(SampleQueryRepository queryRepository) { _queryRepository = queryRepository; RuleFor(x => x) .Must(model => _queryRepository.IsCompliant(model.PropertyA, model.PropertyB)) .WithMessage("Not compliant.") .OverridePropertyName("PropertyA") // 可指定关联属性,或用string.Empty表示全局错误 .OverridePropertyName("PropertyB"); } } // 控制器中使用(需配置FluentValidation依赖注入) public class SampleController : Controller { private readonly IValidator<SampleViewModel> _validator; public SampleController(IValidator<SampleViewModel> validator) { _validator = validator; } [HttpPost] public IActionResult Submit(SampleViewModel model) { var validationResult = _validator.Validate(model); if (!validationResult.IsValid) { validationResult.AddToModelState(ModelState, null); return View(model); } // 执行后续业务逻辑 return RedirectToAction("Success"); } }
内容的提问来源于stack exchange,提问作者Patee Gutee
相关产品推荐
相关产品推荐

