Asp.Net Mvc 中未为 Url 绑定调用自定义验证属性,但适用于 FromBody 绑定

Custom validation attribute is not called in Asp.Net Mvc for Url binding, but works for FromBody binding

我在 Mvc 中观察到一个奇怪的行为,不确定它是错误还是功能。

我有一个客户验证属性 MyAttribute 并使用它,例如像这样:

public async Task<IActionResult> GetData([MyAttribute] string myparam)

令人惊讶的是,如果此参数不在 url 中,则永远不会调用属性(IsValid 函数),但如果参数在 Url 中,则会调用它,但为空(例如 myParam=)。如果我有一个必需的属性,它也会被调用:

public async Task<IActionResult> GetData([MyAttribute, Required] string myparam)

现在,如果我有这样的请求 class:

public class MyRequest
{
    [MyAttribute]
    public string Test {get;set;}
}

public async Task<IActionResult> GetData([FromBody] MyRequest myparam)

然后即使没有 Required 参数也会调用该属性。

这里某处有错误还是有意为之?

首先,如果我们使用Asp.net 核心源代码进行调试,将会非常有帮助。在此处查看更多信息:debug-net-core-source-visual-studio-2019。 这个问题在源码中的关键点是:EnforceBindRequiredAndValidate。 这是源代码:

private void EnforceBindRequiredAndValidate(
            ObjectModelValidator baseObjectValidator,
            ActionContext actionContext,
            ParameterDescriptor parameter,
            ModelMetadata metadata,
            ModelBindingContext modelBindingContext,
            ModelBindingResult modelBindingResult)
        {
            RecalculateModelMetadata(parameter, modelBindingResult, ref metadata);

            if (!modelBindingResult.IsModelSet && metadata.IsBindingRequired)
            {
                // Enforce BindingBehavior.Required (e.g., [BindRequired])
                var modelName = modelBindingContext.FieldName;
                var message = metadata.ModelBindingMessageProvider.MissingBindRequiredValueAccessor(modelName);
                actionContext.ModelState.TryAddModelError(modelName, message);
            }
            else if (modelBindingResult.IsModelSet)
            {
               // Enforce any other validation rules
                baseObjectValidator.Validate(
                    actionContext,
                    modelBindingContext.ValidationState,
                    modelBindingContext.ModelName,
                    modelBindingResult.Model,
                    metadata);
            }
            else if (metadata.IsRequired)
            {
                // We need to special case the model name for cases where a 'fallback' to empty
                // prefix occurred but binding wasn't successful. For these cases there will be no
                // entry in validation state to match and determine the correct key.
                //
                // See https://github.com/aspnet/Mvc/issues/7503
                //
                // This is to avoid adding validation errors for an 'empty' prefix when a simple
                // type fails to bind. The fix for #7503 uncovered this issue, and was likely the
                // original problem being worked around that regressed #7503.
                var modelName = modelBindingContext.ModelName;

                if (string.IsNullOrEmpty(modelBindingContext.ModelName) &&
                    parameter.BindingInfo?.BinderModelName == null)
                {
                    // If we get here then this is a fallback case. The model name wasn't explicitly set
                    // and we ended up with an empty prefix.
                    modelName = modelBindingContext.FieldName;
                }

                // Run validation, we expect this to validate [Required].
                baseObjectValidator.Validate(
                    actionContext,
                    modelBindingContext.ValidationState,
                    modelName,
                    modelBindingResult.Model,
                    metadata);
            }
        }

如果我们添加 required 属性 条件 'else if(metadata.IsRequired)' 将被命中,然后它将调用 Validate 函数。

这里是issue请求和普通请求的区别: modelBindingResult 的值非常重要。如果结果包含成功,那么它将在函数 EnforceBindRequiredAndValidate 中调用验证

从源代码中我们可以发现这是设计使然的行为。

  var modelBindingContext = DefaultModelBindingContext.CreateBindingContext(
                actionContext,
                valueProvider,
                metadata,
                parameter.BindingInfo,
                parameter.Name);

modelBindingResult 来自上面的上下文。你会发现我们需要提供相关的provider和parameter然后modelBindingResult.IsModelSet会被设置为true,此时函数validate会被调用