ASP.NET Core MVC:扩展RequiredAttribute的验证逻辑
希望在强类型视图模型中扩展RequiredAttribute的功能。现有引用数据视图模型如下:
public class ReferenceDataViewModel { public int? Id { get; set; } public string? Name { get; set; } }
它被用于其他视图模型,示例:
public class MyEditViewModel { // 可选:该属性可为null,其Id值也可为null public ReferenceDataViewModel? OptionalRef { get; set; } // 必填:该属性本身不能为null,且其Id值也不能为null [Required] public ReferenceDataViewModel? RequiredRef { get; set; } }
针对RequiredRef,内置的RequiredAttribute仅能验证属性本身不为null,但还需要确保其Id也不为null。
已尝试通过自定义ValidationAttribute实现需求(代码如下),但为了一致性,希望直接扩展内置的RequiredAttribute:
public class ExtendedRequiredAttribute : RequiredAttribute { public ExtendedRequiredAttribute() { } public ExtendedRequiredAttribute(RequiredAttribute attributeToCopy) { AllowEmptyStrings = attributeToCopy.AllowEmptyStrings; if (attributeToCopy.ErrorMessage != null) { ErrorMessage = attributeToCopy.ErrorMessage; } if (attributeToCopy.ErrorMessageResourceType != null) { ErrorMessageResourceName = attributeToCopy.ErrorMessageResourceName; ErrorMessageResourceType = attributeToCopy.ErrorMessageResourceType; } } public override bool IsValid(object? value) { if (value is ReferenceDataViewModel entityReference) { return entityReference?.Id != null; } return base.IsValid(value); } }
之后尝试使用AttributeAdapter(该方式在ASP.NET MVC中有效)但未生效,代码如下:
public class ExtendedRequiredAttributeAdapter : AttributeAdapterBase<RequiredAttribute> { public ExtendedRequiredAttributeAdapter(ExtendedRequiredAttribute attribute, IStringLocalizer? stringLocalizer) : base(attribute, stringLocalizer) { } public override void AddValidation(ClientModelValidationContext context) { MergeAttribute(context.Attributes, "data-val", "true"); MergeAttribute(context.Attributes, "data-val-required", GetErrorMessage(context)); } public override string GetErrorMessage(ModelValidationContextBase validationContext) { ArgumentNullException.ThrowIfNull(validationContext); return GetErrorMessage(validationContext.ModelMetadata, validationContext.ModelMetadata.GetDisplayName()); } } public class CustomValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider { private readonly IValidationAttributeAdapterProvider _innerProvider = new ValidationAttributeAdapterProvider(); public IAttributeAdapter? GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer? stringLocalizer) { var type = attribute.GetType(); if (type == typeof(RequiredAttribute)) { var requiredAttribute = (RequiredAttribute)attribute; var extendedRequiredAttrib = new ExtendedRequiredAttribute(requiredAttribute); return new ExtendedRequiredAttributeAdapter(extendedRequiredAttrib, stringLocalizer); } return _innerProvider.GetAttributeAdapter(attribute, stringLocalizer); } }
已在启动时注册:
services.AddSingleton<IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();
请问如何正确扩展内置RequiredAttribute以实现需求?
你的思路方向是对的,但目前实现仅覆盖了客户端验证适配,服务器端验证逻辑并未被替换。IValidationAttributeAdapterProvider仅负责将验证属性映射到客户端验证规则,服务器端验证仍由原始RequiredAttribute执行,因此扩展逻辑未生效。
要完整实现需求,需同时替换服务器端验证逻辑和客户端适配,具体步骤如下:
1. 替换服务器端验证逻辑
ASP.NET Core中,服务器端验证由ModelValidatorProvider处理,需自定义一个提供者,将所有RequiredAttribute替换为你的ExtendedRequiredAttribute:
public class ExtendedRequiredModelValidatorProvider : IModelValidatorProvider { public void CreateValidators(ModelValidatorProviderContext context) { // 遍历所有已注册的验证器 for (int i = context.Results.Count - 1; i >= 0; i--) { var validatorItem = context.Results[i]; // 找到基于RequiredAttribute的验证器 if (validatorItem.Validator is RequiredAttributeValidator) { // 获取原始的RequiredAttribute if (validatorItem.ValidatorMetadata is RequiredAttribute originalAttribute) { // 创建扩展后的属性 var extendedAttribute = new ExtendedRequiredAttribute(originalAttribute); // 替换验证器为基于扩展属性的RequiredAttributeValidator context.Results[i] = new ValidatorItem { Validator = new RequiredAttributeValidator(extendedAttribute), ValidatorMetadata = extendedAttribute }; } } } } }
2. 注册自定义的ModelValidatorProvider
在启动类的ConfigureServices中,添加自定义验证器提供者:
services.AddControllersWithViews(options => { options.ModelValidatorProviders.Add(new ExtendedRequiredModelValidatorProvider()); }); // 保留之前注册的适配器提供者 services.AddSingleton<IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();
3. 修正AttributeAdapter的泛型类型
将ExtendedRequiredAttributeAdapter的泛型参数改为ExtendedRequiredAttribute,避免潜在类型问题:
public class ExtendedRequiredAttributeAdapter : AttributeAdapterBase<ExtendedRequiredAttribute> { public ExtendedRequiredAttributeAdapter(ExtendedRequiredAttribute attribute, IStringLocalizer? stringLocalizer) : base(attribute, stringLocalizer) { } public override void AddValidation(ClientModelValidationContext context) { MergeAttribute(context.Attributes, "data-val", "true"); MergeAttribute(context.Attributes, "data-val-required", GetErrorMessage(context)); // 若需客户端验证Id,添加自定义验证属性 MergeAttribute(context.Attributes, "data-val-required-refid", "请选择有效的引用数据"); } public override string GetErrorMessage(ModelValidationContextBase validationContext) { ArgumentNullException.ThrowIfNull(validationContext); return GetErrorMessage(validationContext.ModelMetadata, validationContext.ModelMetadata.GetDisplayName()); } }
4. 客户端验证补充(可选)
若需客户端也验证Id不为null,添加对应的JavaScript逻辑:
$.validator.addMethod("required-refid", function (value, element, params) { // 假设引用数据的Id字段对应元素的name以.Id结尾 var idElement = $(element).closest('.reference-data').find('[name$=".Id"]'); return idElement.val() !== "" && idElement.val() !== null; }); $.validator.unobtrusive.adapters.addBool("required-refid");
关键说明
IValidationAttributeAdapterProvider仅负责客户端验证规则生成,服务器端验证由ModelValidatorProvider管理的验证器执行,必须同时替换两部分。- 通过替换
RequiredAttributeValidator的元数据为ExtendedRequiredAttribute,服务器端验证时会执行你重写的IsValid方法。 - 修改后,所有标注
[Required]的ReferenceDataViewModel类型属性,会自动验证属性本身和Id都不为null,无需替换为自定义属性,保证了一致性。
内容的提问来源于stack exchange,提问作者RikRak

