You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 21:40:30