ASP.NET Core MVC — MVC执行概貌(3) ControllerActionInvker与模型绑定、模型校验

ControllerActionInvker中有一个字段_arguments,类型为Dictionary<string, object>,用于存放参数绑定的结构。该类提供了BinArgumentsAsync()方法用于绑定参数:

private Task BindArgumentsAsync()
{
    var actionDescriptor = _controllerContext.ActionDescriptor;
    if (actionDescriptor.BoundProperties.Count == 0 && actionDescriptor.Parameters.Count == 0) {
        return Task.CompletedTask;
    }
    Debug.Assert(_cacheEntry.ControllerBinderDelegate != null);
    return _cacheEntry.ControllerBinderDelegate(_controllerContext, _instance, _arguments);
}

该方法执行后,会为字段_arguments逐一添加以参数名为键名的键值。这里的ControllerBinderDelegate是一个委托类型,负责绑定参数:

internal delegate Task ControllerBinderDelegate(ControllerContext controllerContext, object controller, Dictionary<string, object> arguments);

该委托的实例由ControllerBinderDelegateProvider的静态方法CreateBinderDelegate()提供:

internal static class ControllerBinderDelegateProvider
{
    public static ControllerBinderDelegate CreateBinderDelegate(ParameterBinder parameterBinder, IModelBinderFactory modelBinderFactory, IModelMetadataProvider modelMetadataProvider, ControllerActionDescriptor actionDescriptor, MvcOptions mvcOptions)
    {
        // ... check null
        
        var parameterBindingInfo = GetParameterBindingInfo(modelBinderFactory, modelMetadataProvider, actionDescriptor, mvcOptions);
        var propertyBindingInfo = GetPropertyBindingInfo(modelBinderFactory, modelMetadataProvider, actionDescriptor);
        if (parameterBindingInfo == null && propertyBindingInfo == null)
        {
            return null;
        }
        return Bind;
    }
}

可以看到,这里的有两个重要的方法调用:GetParameterBindingInfo()GetPropertyBindingInfo(),分别用于生成参数绑定信息和属性绑定信息:

private static BinderItem[] GetParameterBindingInfo( IModelBinderFactory modelBinderFactory, IModelMetadataProvider modelMetadataProvider, ControllerActionDescriptor actionDescriptor, MvcOptions mvcOptions)
{
    var parameters = actionDescriptor.Parameters;
    if (parameters.Count == 0) { return null; }
    var parameterBindingInfo = new BinderItem[parameters.Count];
    for (var i = 0; i < parameters.Count; i++)
    {
        var parameter = parameters[i];
        ModelMetadata metadata;
        if (mvcOptions.AllowValidatingTopLevelNodes &&
            modelMetadataProvider is ModelMetadataProvider modelMetadataProviderBase &&
            parameter is ControllerParameterDescriptor controllerParameterDescriptor)
        {
            // The default model metadata provider derives from ModelMetadataProvider
            // and can therefore supply information about attributes applied to parameters.
            metadata = modelMetadataProviderBase.GetMetadataForParameter(controllerParameterDescriptor.ParameterInfo);
        }
        else
        {
            // For backward compatibility, if there's a custom model metadata provider that
            // only implements the older IModelMetadataProvider interface, access the more
            // limited metadata information it supplies. In this scenario, validation attributes
            // are not supported on parameters.
            metadata = modelMetadataProvider.GetMetadataForType(parameter.ParameterType);
        }
        var binder = modelBinderFactory.CreateBinder(new ModelBinderFactoryContext
        {
            BindingInfo = parameter.BindingInfo,
            Metadata = metadata,
            CacheToken = parameter,
        });
        parameterBindingInfo[i] = new BinderItem(binder, metadata);
    }
    return parameterBindingInfo;
}
private static BinderItem[] GetPropertyBindingInfo( IModelBinderFactory modelBinderFactory, IModelMetadataProvider modelMetadataProvider, ControllerActionDescriptor actionDescriptor)
{
    var properties = actionDescriptor.BoundProperties;
    if (properties.Count == 0) { return null; }
    var propertyBindingInfo = new BinderItem[properties.Count];
    var controllerType = actionDescriptor.ControllerTypeInfo.AsType();
    for (var i = 0; i < properties.Count; i++)
    {
        var property = properties[i];
        var metadata = modelMetadataProvider.GetMetadataForProperty(controllerType, property.Name);
        var binder = modelBinderFactory.CreateBinder(new ModelBinderFactoryContext
        {
            BindingInfo = property.BindingInfo,
            Metadata = metadata,
            CacheToken = property,
        });
        propertyBindingInfo[i] = new BinderItem(binder, metadata);
    }
    return propertyBindingInfo;
}

CreateBinderDelegate方法调用的最后返回了一个Bind函数,这个Bind的实现是:

async Task Bind(ControllerContext controllerContext, object controller, Dictionary<string, object> arguments)
{
    var valueProvider = await CompositeValueProvider.CreateAsync(controllerContext);
    var parameters = actionDescriptor.Parameters;
    for (var i = 0; i < parameters.Count; i++)
    {
        var parameter = parameters[i];
        var bindingInfo = parameterBindingInfo[i];
        var modelMetadata = bindingInfo.ModelMetadata;
        if (!modelMetadata.IsBindingAllowed) { continue; }
        var result = await parameterBinder.BindModelAsync(controllerContext, bindingInfo.ModelBinder, valueProvider, parameter, modelMetadata, value: null);
        if (result.IsModelSet)
        {
            arguments[parameter.Name] = result.Model;
        }
    }
    var properties = actionDescriptor.BoundProperties;
    for (var i = 0; i < properties.Count; i++)
    {
        var property = properties[i];
        var bindingInfo = propertyBindingInfo[i];
        var modelMetadata = bindingInfo.ModelMetadata;
        if (!modelMetadata.IsBindingAllowed) { continue; }
        var result = await parameterBinder.BindModelAsync(controllerContext, bindingInfo.ModelBinder, valueProvider, property, modelMetadata, value: null);
        if (result.IsModelSet)
        {
            PropertyValueSetter.SetValue(bindingInfo.ModelMetadata, controller, result.Model);
        }
    }
}
}

内部调用的是ParameterBinder类,故名思义,该类负责绑定参数。其BindModelAsync()有两部分工作:

  1. 模型绑定:ParameterBinder类通过依赖注入传入一个IModelMetadataProviderIModelBinderFactory,然后由IModelMetadataProvider创建metadata,再由BinderFactory生成binder,最后调用binder绑定参数,
  2. 模型校验:ParameterBinder类通过依赖注入传入一个IObjectModelValidator实例,在modelBinder.BindModelAsync()完成之后,通过该IObjectModelValidator实例来完成模型校验:
public virtual async Task<ModelBindingResult> BindModelAsync( ActionContext actionContext, IModelBinder modelBinder, IValueProvider valueProvider, ParameterDescriptor parameter, ModelMetadata metadata, object value)
{
    ... check null
                if (parameter.BindingInfo?.RequestPredicate?.Invoke(actionContext) == false)
    {
        Logger.ParameterBinderRequestPredicateShortCircuit(parameter, metadata);
        return ModelBindingResult.Failed();
    }
    var modelBindingContext = DefaultModelBindingContext.CreateBindingContext( actionContext, valueProvider, metadata, parameter.BindingInfo, parameter.Name);
    modelBindingContext.Model = value;
    var parameterModelName = parameter.BindingInfo?.BinderModelName ?? metadata.BinderModelName;
    if (parameterModelName != null)
    {
        // The name was set explicitly, always use that as the prefix.
        modelBindingContext.ModelName = parameterModelName;
    }
    else if (modelBindingContext.ValueProvider.ContainsPrefix(parameter.Name))
    {
        // We have a match for the parameter name, use that as that prefix.
        modelBindingContext.ModelName = parameter.Name;
    }
    else
    {
        // No match, fallback to empty string as the prefix.
        modelBindingContext.ModelName = string.Empty;
    }
    await modelBinder.BindModelAsync(modelBindingContext);
    Logger.DoneAttemptingToBindParameterOrProperty(parameter, metadata);
    var modelBindingResult = modelBindingContext.Result;
    if (_objectModelValidator is ObjectModelValidator baseObjectValidator)
    {
        Logger.AttemptingToValidateParameterOrProperty(parameter, metadata);
        EnforceBindRequiredAndValidate( baseObjectValidator, actionContext, parameter, metadata, modelBindingContext, modelBindingResult);
        Logger.DoneAttemptingToValidateParameterOrProperty(parameter, metadata);
    }
    else
    {
        // For legacy implementations (which directly implemented IObjectModelValidator), fall back to the
        // back-compatibility logic. In this scenario, top-level validation attributes will be ignored like
        // they were historically.
        if (modelBindingResult.IsModelSet)
        {
            _objectModelValidator.Validate( actionContext, modelBindingContext.ValidationState, modelBindingContext.ModelName, modelBindingResult.Model);
        }
    }
    return modelBindingResult;
}

这里的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);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值