如何扩展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
相关产品推荐
相关产品推荐

