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

如何扩展ASP.NET Core验证:忽略Optional未设置属性并验证其Value

问题描述

我使用了Roslyn的Optional<T>结构体实现ASP.NET请求模型的部分更新——允许用户在更新请求中仅设置需要修改的属性。结构体核心代码如下:

public readonly struct Optional<T>
{
    public Optional(T value)
    {
        _hasValue = true;
        _value = value;
    }

    public bool HasValue => _hasValue;

    public T Value => _hasValue 
        ? _value
        : throw new InvalidOperationException("Optional object must have a value");
}

但添加标准验证特性时遇到两个问题:

  • 当字段未设置时,验证逻辑尝试读取Value属性会抛出异常
  • 当字段已设置时,验证特性会尝试验证Optional<T>结构体本身,而非其内部的Value

补充说明:无法使用Nullable<T>,因为需要区分**字段为null(表示将该字段更新为null)和字段不存在(表示不修改该字段)**两种场景,模拟JSON中的undefined。例如可接受null的字段会用Optional<string?>或Optional<long?>。


解决方案

1. 自定义验证属性适配Optional<T>

创建通用验证属性,自动处理Optional<T>的验证逻辑:

public class OptionalValidationAttribute : ValidationAttribute
{
    private readonly ValidationAttribute _innerAttribute;

    public OptionalValidationAttribute(Type innerAttributeType, params object[] args)
    {
        _innerAttribute = (ValidationAttribute)Activator.CreateInstance(innerAttributeType, args);
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value is not Optional<object> optional)
        {
            return _innerAttribute.GetValidationResult(value, validationContext);
        }

        // 未设置值时跳过验证
        if (!optional.HasValue)
        {
            return ValidationResult.Success;
        }

        // 已设置值时验证内部Value
        return _innerAttribute.GetValidationResult(optional.Value, validationContext);
    }
}

使用示例:

public class UpdateUserRequest
{
    [OptionalValidation(typeof(MaxLengthAttribute), 50)]
    public Optional<string?> Username { get; set; }

    [OptionalValidation(typeof(RangeAttribute), 18, 120)]
    public Optional<int?> Age { get; set; }
}

2. 实现IValidatableObject手动控制验证

若不想创建自定义属性,可让请求模型实现IValidatableObject,手动处理每个Optional<T>字段的验证:

public class UpdateUserRequest : IValidatableObject
{
    public Optional<string?> Username { get; set; }
    public Optional<int?> Age { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        // 验证Username:已设置时检查长度
        if (Username.HasValue)
        {
            if (!string.IsNullOrEmpty(Username.Value) && Username.Value.Length > 50)
            {
                yield return new ValidationResult("用户名长度不能超过50", new[] { nameof(Username) });
            }
        }

        // 验证Age:已设置时检查范围
        if (Age.HasValue && Age.Value.HasValue)
        {
            if (Age.Value < 18 || Age.Value > 120)
            {
                yield return new ValidationResult("年龄必须在18到120之间", new[] { nameof(Age) });
            }
        }
    }
}

3. 自定义ModelValidatorProvider全局适配

若希望全局所有Optional<T>字段自动适配验证,可自定义ModelValidatorProvider:

// 先给Optional<T>添加接口,方便验证逻辑识别
public interface IOptional
{
    bool HasValue { get; }
    object Value { get; }
}

// 修改Optional<T>实现该接口
public readonly struct Optional<T> : IOptional
{
    private readonly bool _hasValue;
    private readonly T _value;

    public Optional(T value)
    {
        _hasValue = true;
        _value = value;
    }

    public bool HasValue => _hasValue;

    public T Value => _hasValue 
        ? _value
        : throw new InvalidOperationException("Optional object must have a value");

    // 显式实现IOptional接口
    object IOptional.Value => Value!;
}

// 自定义验证器提供器
public class OptionalModelValidatorProvider : IModelValidatorProvider
{
    public void CreateValidators(ModelValidatorProviderContext context)
    {
        if (context.ModelMetadata.ModelType.IsGenericType && 
            context.ModelMetadata.ModelType.GetGenericTypeDefinition() == typeof(Optional<>))
        {
            context.Validators.Clear();
            context.Validators.Add(new OptionalModelValidator(context.ModelMetadata));
        }
    }

    private class OptionalModelValidator : IModelValidator
    {
        private readonly ModelMetadata _metadata;

        public OptionalModelValidator(ModelMetadata metadata)
        {
            _metadata = metadata;
        }

        public IEnumerable<ModelValidationResult> Validate(ModelValidationContext context)
        {
            if (context.Model is not IOptional optional)
            {
                yield break;
            }

            if (!optional.HasValue)
            {
                yield break;
            }

            // 获取内部值的元数据并执行验证
            var innerType = _metadata.ModelType.GetGenericArguments()[0];
            var innerMetadata = context.MetadataProvider.GetMetadataForType(innerType);
            var innerValidators = context.ValidatorProvider.GetValidators(innerMetadata);

            foreach (var validator in innerValidators)
            {
                foreach (var result in validator.Validate(new ModelValidationContext(context.ActionContext, innerMetadata, context.MetadataProvider, optional.Value)))
                {
                    yield return result;
                }
            }
        }
    }
}

在Program.cs/Startup.cs中注册全局验证器:

builder.Services.AddMvc(options =>
{
    options.ModelValidatorProviders.Add(new OptionalModelValidatorProvider());
});

内容的提问来源于stack exchange,提问作者Ilya Chernomordik

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 16:46:19