如何在 ValidationHtmlAttributeProvider 中的 AddValidationAttributes 期间获取 parent object?

How to get parent object during AddValidationAttributes in ValidationHtmlAttributeProvider?

我正在尝试在 ASP.NET 核心 MVC 网络应用程序中实现我的自定义验证属性,包括服务器端和客户端验证。现有数据注释的缺点是属性的静态性质,这使得无法将运行时数据传递给它。

我想创建 GreaterThanAttribute,它以另一个 属性 的名称作为参数,将给定值与 属性 的值进行比较。像这样:

public class TestModel 
{
    public int PropertyA { get; set; }

    [GreaterThan(nameof(PropertyA))]
    public int PropertyB { get; set; }
}

我通过以下方式实现了该属性:

[AttributeUsage(AttributeTargets.Property)]
public class GreaterThanAttribute : ValidationAttribute, IClientModelValidator
{
    public const string RuleName = "greaterthan";
    public const string ParameterName = "othervalue";
    public object OtherPropertyValue { get; set; }
    public string OtherPropertyName { get; }

    public GreaterThanAttribute(string otherPropertyName)
    {
        OtherPropertyName = otherPropertyName;
        ErrorMessage = $"The value should be greater than filed {OtherPropertyName}";
    }

    public void AddValidation(ClientModelValidationContext context)
    {
        if (context == null)
            throw new ArgumentNullException(nameof(context));

        MergeAttribute(context.Attributes, "data-val", "true");
        MergeAttribute(context.Attributes, $"data-val-{RuleName}", ErrorMessageString);
        MergeAttribute(context.Attributes, $"data-val-{RuleName}-{ParameterName}", OtherPropertyValue?.ToString());
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        // server side validation, no difficulties here
    }

    private static void MergeAttribute(IDictionary<string, string> attributes, string key, string value)
    {
        if (attributes.ContainsKey(key))
            return;
        attributes.Add(key, value);
    }
}

在我看来,我已经为此属性添加了不显眼的验证器:

<script type="text/javascript">
$.validator.addMethod("@GreaterThanAttribute.RuleName", function (value, element, params) {
    var parsedThisValue = Globalize.numberParser()(value);
    var parsedOtherValue = Globalize.numberParser()(params);
    return parsedThisValue > parsedOtherValue;
});
$.validator.unobtrusive.adapters.addSingleVal("@GreaterThanAttribute.RuleName", "@GreaterThanAttribute.ParameterName");
</script>

现在的问题是我需要手动将 data-val-greaterthan-othervalue 属性添加到输入字段,例如:

@{
    var propertyName = $"data-val-{GreaterThanAttribute.RuleName}-{GreaterThanAttribute.ParameterName}";
    var propertyValue = Model.Child[0].PropertyB;
}
@Html.TextBoxFor(x => Model.Child[0].PropertyA, new Dictionary<string, object> {{propertyName, propertyValue}})
@Html.ValidationMessageFor(x => Model.Child[0].PropertyA)

我不喜欢那样。因此,我正在寻找一种使用现有 ASP.NET 机制添加此属性而不会污染视图的方法。 我在答案 .

中找到的最接近的方法

现在我正在尝试将实际值注入我的自定义验证中的属性 html 属性提供程序:

public class CustomValidationHtmlAttributeProvider : DefaultValidationHtmlAttributeProvider
{
    private readonly IOptions<MvcViewOptions> _optionsAccessor;
    private readonly IModelMetadataProvider _metadataProvider;
    private readonly ClientValidatorCache _clientValidatorCache;


    public CustomValidationHtmlAttributeProvider(IOptions<MvcViewOptions> optionsAccessor, IModelMetadataProvider metadataProvider, ClientValidatorCache clientValidatorCache) 
        : base(optionsAccessor, metadataProvider, clientValidatorCache)
    {
        _optionsAccessor = optionsAccessor;
        _metadataProvider = metadataProvider;
        _clientValidatorCache = clientValidatorCache;
    }

    public override void AddValidationAttributes(ViewContext viewContext, ModelExplorer modelExplorer, IDictionary<string, string> attributes)
    {
        // getting existing validation attribute
        var greaterThanAttribute = modelExplorer.Metadata.ValidatorMetadata.FirstOrDefault(x =>
            x.GetType() == typeof(GreaterThanAttribute)) as GreaterThanAttribute;
        var otherPropertyName = greaterThanAttribute.OtherPropertyName;

        // -------------
        // how to get reference to parent object of the model here?
        // -------------
        var otherValue = ?????????????

        greaterThanAttribute.OtherPropertyValue = otherValue;

        base.AddValidationAttributes(viewContext, modelExplorer, attributes);
    }
}

问题是我无法找到一种方法来获取对正在验证的 属性 的 parent class 的引用以获得 PropertyB 的值.我这里只有:

所以问题是:考虑到我不知道容器的整个层次结构这一事实,如何在此处达到 PropertyB 的值?也许还有其他完整的方法来实现所需的验证属性?

我终于找到了解决办法。

首先,我重载了错误版本的方法。正确的是 void AddAndTrackValidationAttributes(ViewContext viewContext, ModelExplorer modelExplorer, string expression, IDictionary<string, string> attributes),因为它向当前 属性 提供带有验证属性的表达式字符串。

通过这个表达式,我们可以假设名称存储在 otherPropertyName 中的目标 属性 存在于相同的路径中(因为它位于相同的 class 中)。例如,如果表达式是 Model.Child[0].PropertyA,则可以使用表达式 Model.Child[0].PropertyB 检索目标 属性。

不幸的是,ExpressionMetadataProvider.FromStringExpression 函数不适用于带有索引器的表达式。但是,它仅适用于属性表达式。在我的例子中,唯一的方法是手动遍历对象层次结构:解析表达式并使用反射转到给定索引处的属性和元素。

全部代码:

public class CustomValidationHtmlAttributeProvider : DefaultValidationHtmlAttributeProvider
{
    private readonly IOptions<MvcViewOptions> _optionsAccessor;
    private readonly IModelMetadataProvider _metadataProvider;
    private readonly ClientValidatorCache _clientValidatorCache;


    public CustomValidationHtmlAttributeProvider(IOptions<MvcViewOptions> optionsAccessor, IModelMetadataProvider metadataProvider, ClientValidatorCache clientValidatorCache) 
        : base(optionsAccessor, metadataProvider, clientValidatorCache)
    {
        _optionsAccessor = optionsAccessor;
        _metadataProvider = metadataProvider;
        _clientValidatorCache = clientValidatorCache;
    }

    public override void AddValidationAttributes(ViewContext viewContext, ModelExplorer modelExplorer, string expression, IDictionary<string, string> attributes)
    {
        // getting existing validation attribute
        var greaterThanAttribute = modelExplorer.Metadata.ValidatorMetadata.FirstOrDefault(x =>
            x.GetType() == typeof(GreaterThanAttribute)) as GreaterThanAttribute;
        var otherPropertyName = greaterThanAttribute.OtherPropertyName;

        var targetExpression = GetTargetPropertyExpression(expression, otherPropertyName);
        var otherValue = GetValueOnExpression(modelExplorer.Container.Model, targetExpression);

        greaterThanAttribute.OtherPropertyValue = otherValue;

        base.AddValidationAttributes(viewContext, modelExplorer, attributes);
    }

    private static object GetValueOnExpression(object container, string expression)
    {
        while (expression != "")
        {
            if (NextStatementIsIndexer(expression))
            {
                var index = GetIndex(expression);

                switch (container)
                {
                    case IDictionary dictionary:
                        container = dictionary[index];
                        break;
                    case IEnumerable<object> enumerable:
                        container = enumerable.ElementAt(int.Parse(index));
                        break;
                    default:
                        throw new Exception($"{container} is unknown collection type");
                }

                expression = ClearIndexerStatement(expression);
            }
            else
            {
                var propertyName = GetPropertyStatement(expression);
                var propertyInfo = container.GetType().GetProperty(propertyName);
                if (propertyInfo == null)
                    throw new Exception($"Can't find {propertyName} property in the container {container}");

                container = propertyInfo.GetValue(container);
                expression = ClearPropertyStatement(expression);
            }
        }

        return container;
    }

    private static bool NextStatementIsIndexer(string expression) 
        => expression[0] == '[';

    private static string ClearPropertyStatement(string expression)
    {
        var statementEndPosition = expression.IndexOfAny(new [] {'.', '['});
        if (statementEndPosition == -1) return "";
        if (expression[statementEndPosition] == '.') statementEndPosition++;
        return expression.Substring(statementEndPosition);
    }

    private static string GetPropertyStatement(string expression)
    {
        var statementEndPosition = expression.IndexOfAny(new [] {'.', '['});
        if (statementEndPosition == -1) return expression;
        return expression.Substring(0, statementEndPosition);
    }

    private static string ClearIndexerStatement(string expression)
    {
        var statementEndPosition = expression.IndexOf(']');
        if (statementEndPosition == expression.Length - 1) return "";
        if (expression[statementEndPosition + 1] == '.') statementEndPosition++;
        return expression.Substring(statementEndPosition + 1);
    }

    private static string GetIndex(string expression)
    {
        var closeBracketPosition = expression.IndexOf(']');
        return expression.Substring(1, closeBracketPosition - 1);
    }

    private static string GetTargetPropertyExpression(string sourceExpression, string targetProperty)
    {
        var memberAccessTokenPosition = sourceExpression.LastIndexOf('.');
        if (memberAccessTokenPosition == -1) // expression is just a property name
            return targetProperty;
        var newExpression = sourceExpression.Substring(0, memberAccessTokenPosition + 1) + targetProperty;
        return newExpression;
    }
}