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

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

1
2
3
4
5
6
7
8
9
10
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是一个委托类型,负责绑定参数:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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(),分别用于生成参数绑定信息和属性绑定信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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的实现是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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实例来完成模型校验:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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);
}
}