如何在.NET 6中实现基于XML的自定义模型验证
.NET 6 自定义动态模型验证失效问题
我通过自定义类特性实现模型验证,将验证规则存储在XML文件中:自定义类特性指定XML文件路径,内置读取XML获取对应类验证规则的逻辑,调用Model.IsValid()时执行验证。该方案在.NET 4.5 MVC中可正常运行,但迁移到.NET 6后失效,相关代码如下:
public class CustomDynamicModelValidatorProvider : DataAnnotationsModelValidatorProvider, IModelValidatorProvider { protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders, IEnumerable<Attribute> attributes) { string resourceKeyPath = string.Empty; IList<Attribute> newAttributes = new List<Attribute>(attributes); CustomDynamicValidatorsAttribute resourceMapperAttr = attributes.FirstOrDefault(a => a is CustomDynamicValidatorsAttribute) as CustomDynamicValidatorsAttribute; if (resourceMapperAttr == null) { System.Reflection.MemberInfo classInfo = metadata.ContainerType; if (classInfo != null) { var resourceMapperRootAttr = classInfo.GetCustomAttributes(typeof(ResourceMappingRootAttribute), false).FirstOrDefault() as ResourceMappingRootAttribute; if (resourceMapperRootAttr != null) { resourceKeyPath = resourceMapperRootAttr.Path + "." + metadata.PropertyName; } } } else { resourceKeyPath = resourceMapperAttr.ResourceKeyPath; } if (!string.IsNullOrEmpty(resourceKeyPath)) { string[] validators = ResourceManager.GetValidators(resourceKeyPath).Replace(" ", "").Split(','); var maxLength = ResourceManager.GetMaxLength(resourceKeyPath); if (!string.IsNullOrEmpty(maxLength)) { var required = new MaxLengthAttribute(maxLength.ToInteger()); required.ErrorMessage = string.Format(CustomAttributeHelper.GetResourceText("Shared.Messages.MaximumLengthExceeded"), maxLength); newAttributes.Add(required); } for (int i = 0; i < validators.Length; i++) { if (string.Equals(validators[i], "required", StringComparison.OrdinalIgnoreCase)) { var required = new CustomRequiredAttribute(); newAttributes.Add(required); } else if (validators[i].StartsWith("email", StringComparison.OrdinalIgnoreCase)) { var email = new CustomEmailAttribute(); newAttributes.Add(email); } } } return base.GetValidators(metadata, validatorProviders); } public static IDictionary<string, object> GetValidatorAttributes(string validators) { IDictionary<string, object> attributes = new Dictionary<string, object>(); string[] validatorList = !string.IsNullOrEmpty(validators) ? validators.Replace(" ", "").Split(',') : new string[] { }; foreach (var item in validatorList) { if (!attributes.Keys.Contains("data-val")) { attributes.Add("data-val", "true"); } if (string.Equals(item, "required", StringComparison.OrdinalIgnoreCase)) { attributes.Add("data-val-required", CustomAttributeHelper.GetResourceText("Shared.Messages.Mandatory")); } else if (item.StartsWith("email", StringComparison.OrdinalIgnoreCase)) { attributes.Add("data-val-email", CustomAttributeHelper.GetResourceText("Shared.Messages.vEmailField")); } else if (item.StartsWith("phone", StringComparison.OrdinalIgnoreCase)) { attributes.Add("data-val-phone", CustomAttributeHelper.GetResourceText("Shared.Messages.vPhoneField")); } else if (item.StartsWith("zipcode", StringComparison.OrdinalIgnoreCase)) { attributes.Add("data-val-zipcode", CustomAttributeHelper.GetResourceText("Shared.Messages.vZipCodeField")); } else if (item.StartsWith("mark", StringComparison.OrdinalIgnoreCase)) { string min = string.Empty; string max = string.Empty; string rangeValidatorMessage = string.Empty; if (item.Contains("-")) { var rangeArray = item.Split('-'); if (rangeArray.Length > 2) { min = rangeArray[1]; max = rangeArray[2]; rangeValidatorMessage = CustomAttributeHelper.GetResourceText("Shared.Messages.NumberRange"); } else { max = rangeArray[1]; rangeValidatorMessage = CustomAttributeHelper.GetResourceText("Shared.Messages.vNumericLimited"); } } else { max = "10000"; } if (!string.IsNullOrWhiteSpace(min)) { attributes.Add("data-val-range-min", min); } attributes.Add("data-val-range-max", max); attributes.Add("data-val-range", rangeValidatorMessage); } else if (item.StartsWith("number", StringComparison.OrdinalIgnoreCase)) { attributes.Add("data-val-number", CustomAttributeHelper.GetResourceText("Shared.Messages.NumberRange")); } else if (item.StartsWith("number", StringComparison.OrdinalIgnoreCase)) { attributes.Add("data-val-decimal", string.Format(CustomAttributeHelper.GetResourceText("Shared.Messages.DecimalRange"), CustomAttributeHelper.GetResourceText("Shared.Limits.Decimal"))); } else if (item.StartsWith("file-", StringComparison.OrdinalIgnoreCase)) { attributes.Add("data-val-filetype", item.Split('-')[1]); } } return attributes; } public void CreateValidators(ModelValidatorProviderContext context) { } }
问题原因分析
- 接口实现逻辑失效:.NET 6 中
IModelValidatorProvider的核心执行入口是CreateValidators方法,原代码中空实现该方法,导致自定义验证逻辑完全未被触发;而.NET 4.5 MVC依赖的是重写GetValidators的逻辑,框架调用机制已发生变化。 - 服务注册方式变更:.NET 6 的 MVC 服务注册逻辑和.NET Framework 不同,原有的验证提供者注册方式可能未生效,需重新配置。
- 反射API适配问题:.NET 6 中
ModelMetadata的属性获取时机和逻辑有调整,原代码读取类特性的方式可能无法正确获取目标特性。
修复方案
1. 修正CreateValidators方法实现
替换空实现,将原GetValidators中的验证逻辑迁移到该方法中,确保验证逻辑被触发:
public void CreateValidators(ModelValidatorProviderContext context) { foreach (var item in context.Results) { if (item.Validators.Any()) continue; var metadata = item.Metadata; string resourceKeyPath = string.Empty; // 获取属性上的自定义验证特性 CustomDynamicValidatorsAttribute resourceMapperAttr = metadata.Attributes.OfType<CustomDynamicValidatorsAttribute>().FirstOrDefault(); if (resourceMapperAttr == null) { var classInfo = metadata.ContainerType; if (classInfo != null) { var resourceMapperRootAttr = classInfo.GetCustomAttribute<ResourceMappingRootAttribute>(); if (resourceMapperRootAttr != null) { resourceKeyPath = $"{resourceMapperRootAttr.Path}.{metadata.PropertyName}"; } } } else { resourceKeyPath = resourceMapperAttr.ResourceKeyPath; } if (!string.IsNullOrEmpty(resourceKeyPath)) { string[] validators = ResourceManager.GetValidators(resourceKeyPath).Replace(" ", "").Split(','); var maxLength = ResourceManager.GetMaxLength(resourceKeyPath); // 添加MaxLength验证器 if (!string.IsNullOrEmpty(maxLength)) { var maxLengthAttr = new MaxLengthAttribute(int.Parse(maxLength)); maxLengthAttr.ErrorMessage = string.Format(CustomAttributeHelper.GetResourceText("Shared.Messages.MaximumLengthExceeded"), maxLength); item.Validators.Add(new DataAnnotationsModelValidator(context.ValidatorProviders, metadata, maxLengthAttr)); } // 处理其他验证规则 foreach (var validatorKey in validators) { if (string.Equals(validatorKey, "required", StringComparison.OrdinalIgnoreCase)) { var requiredAttr = new CustomRequiredAttribute(); item.Validators.Add(new DataAnnotationsModelValidator(context.ValidatorProviders, metadata, requiredAttr)); } else if (validatorKey.StartsWith("email", StringComparison.OrdinalIgnoreCase)) { var emailAttr = new CustomEmailAttribute(); item.Validators.Add(new DataAnnotationsModelValidator(context.ValidatorProviders, metadata, emailAttr)); } // 其他验证规则的处理逻辑可参照上述格式补充 } } } }
2. 正确注册验证提供者
在Program.cs中配置MVC服务时,添加自定义验证提供者:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddMvc(options => { // 移除默认的DataAnnotations验证提供者(可选,避免重复验证) var defaultProvider = options.ModelValidatorProviders.OfType<DataAnnotationsModelValidatorProvider>().FirstOrDefault(); if (defaultProvider != null) { options.ModelValidatorProviders.Remove(defaultProvider); } // 添加自定义验证提供者 options.ModelValidatorProviders.Add(new CustomDynamicModelValidatorProvider()); }); // 其他服务配置... var app = builder.Build(); // 中间件配置... app.Run();
3. 优化特性读取逻辑
使用.NET 6 原生的泛型反射API,提升特性读取的可靠性:
- 将
classInfo.GetCustomAttributes(typeof(ResourceMappingRootAttribute), false).FirstOrDefault()替换为classInfo.GetCustomAttribute<ResourceMappingRootAttribute>() - 将
attributes.FirstOrDefault(a => a is CustomDynamicValidatorsAttribute)替换为metadata.Attributes.OfType<CustomDynamicValidatorsAttribute>().FirstOrDefault()
内容的提问来源于stack exchange,提问作者Mahesh Chikhale
相关产品推荐
相关产品推荐

