我想扩展
RequiredAttribute
如何与强类型视图模型一起工作。例如,我有一个参考数据视图模型,如下所示:
public class ReferenceDataViewModel
{
public int? Id { get; set; }
public string? Name { get; set; }
}
这可以用于其他视图模型,如下所示:
public class MyEditViewModel
{
// Optional: this property can be null or this property's Id value can be null
public ReferenceViewModel? OptionalRef { get; set; }
// Required: this property should not be null nor should this property's Id value
[Required]
public ReferenceViewModel? RequiredRef { get; set; }
}
使用
RequiredRef
属性本身不应该是 null
(由 RequiredAttribute
处理),而且 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>();
我通过采取稍微不同的路线解决了这个问题。本质上,我通过创建一个
IModelValidator
来利用 MVC 核心验证基础设施,然后通过 IModelValidatorProvider
将其发挥作用,然后在 MvcOptions
中注册该提供程序。这是代码:
public class ReferenceDataViewModelRequiredValidator : IModelValidator
{
public IEnumerable<ModelValidationResult> Validate(ModelValidationContext context)
{
var model = context.Model as ReferenceDataViewModel;
if (model == null || model.Id.HasValue == false)
{
var message = GetErrorMessage(context.ModelMetadata);
yield return new ModelValidationResult("", message);
}
}
private string GetErrorMessage(ModelMetadata metadata)
{
string? message = null;
var parent = metadata.ContainerType;
var modelName = metadata.PropertyName;
if (parent != null && !string.IsNullOrWhiteSpace(modelName))
{
var modelProperty = parent.GetProperty(modelName);
var requiredAttrib = modelProperty?.GetCustomAttribute<RequiredAttribute>();
if (requiredAttrib != null)
{
message = requiredAttrib.ErrorMessage;
if (string.IsNullOrWhiteSpace(message))
{
message = requiredAttrib.FormatErrorMessage(modelName);
}
}
}
if (string.IsNullOrWhiteSpace(message))
{
message = $"{metadata.GetDisplayName()} is required.";
}
return message;
}
}
这可以通过
ModelValidatorProvider
: 集成到 MVC 的验证中
public class MyModelValidatorProvider : IModelValidatorProvider
{
public void CreateValidators(ModelValidatorProviderContext context)
{
if (HasRequiredAttribute(context.ModelMetadata))
{
if (context.ModelMetadata.ModelType == typeof(ReferenceDataViewModel))
{
var validatorItem = new ValidatorItem()
{
IsReusable = true,
Validator = new ReferenceDataViewModelRequiredValidator()
};
// put the validator in early
context.Results.Insert(0, validatorItem);
}
}
/// <summary>
/// Determines if the property being validated has a [Required] attribute
/// </summary>
/// <remarks>
/// There is an IsRequired property on the <see cref="ModelMetadata"/>, but always seems to be "true".
/// Not sure why.
/// </remarks>
private static bool HasRequiredAttribute(ModelMetadata metadata)
{
if (metadata.ContainerType != null && !string.IsNullOrWhiteSpace(metadata.PropertyName))
{
var property = metadata.ContainerType.GetProperty(metadata.PropertyName);
return property?.HasAttribute<RequiredAttribute>() ?? false;
}
return false;
}
}
启动时需要注册
ModelValidatorProvider
:
builder
.AddControllersWithViews(opts =>
{
opt.ModelValidatorProviders.Add(new MyModelValidatorProvider())
});