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

如何在.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)
    { 
    }

}

问题原因分析

  1. 接口实现逻辑失效:.NET 6 中IModelValidatorProvider的核心执行入口是CreateValidators方法,原代码中空实现该方法,导致自定义验证逻辑完全未被触发;而.NET 4.5 MVC依赖的是重写GetValidators的逻辑,框架调用机制已发生变化。
  2. 服务注册方式变更:.NET 6 的 MVC 服务注册逻辑和.NET Framework 不同,原有的验证提供者注册方式可能未生效,需重新配置。
  3. 反射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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 07:40:34