ASP.NET Core MVC中所提供的Model Binding功能簡單但實用,其主要目的是將請求中包含的數據映射到action的方法參數中。這樣就避免了開發者像在Web Forms時代那樣需要從Request類中手動獲取數據的繁鎖操作,直接提高了開發效率。此功能繼承自ASP.NET MVC,所 ...
ASP.NET Core MVC中所提供的Model Binding功能簡單但實用,其主要目的是將請求中包含的數據映射到action的方法參數中。這樣就避免了開發者像在Web Forms時代那樣需要從Request類中手動獲取數據的繁鎖操作,直接提高了開發效率。此功能繼承自ASP.NET MVC,所以熟悉上一代框架開發的工程師,可以毫無障礙地繼續享有它的便利。
本文想要探索下Model Binding相關的內容,這裡先從源碼中找到其發生的時機與場合。
在ControllerActionInvoker類的Next方法內部,可以看到對BindArgumentsAsync方法的調用,這裡即會對方法的參數進行綁定數據的處理。
private Task Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
{
switch (next)
{
case State.ActionBegin:
{
var controllerContext = _controllerContext;
_cursor.Reset();
_instance = _cacheEntry.ControllerFactory(controllerContext);
_arguments = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
var task = BindArgumentsAsync();
if (task.Status != TaskStatus.RanToCompletion)
{
next = State.ActionNext;
return task;
}
goto case State.ActionNext;
}
...
}
}
此方法又調用了ControllerActionInvokerCacheEntry類中ControllerBinderDelegate屬性,該屬性是一個delegate方法,所以傳入參數後可直接執行處理。
private Task BindArgumentsAsync()
{
...
return _cacheEntry.ControllerBinderDelegate(_controllerContext, _instance, _arguments);
}
創建ControllerActionInvokerCacheEntry的地方是前兩篇文章(Controller,Action)中已經提到過的ControllerActionInvokerCache類。
public (ControllerActionInvokerCacheEntry cacheEntry, IFilterMetadata[] filters) GetCachedResult(ControllerContext controllerContext)
{
...
if (!cache.Entries.TryGetValue(actionDescriptor, out var cacheEntry))
{
...
var propertyBinderFactory = ControllerBinderDelegateProvider.CreateBinderDelegate(
_parameterBinder,
_modelBinderFactory,
_modelMetadataProvider,
actionDescriptor);
var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor);
cacheEntry = new ControllerActionInvokerCacheEntry(
filterFactoryResult.CacheableFilters,
controllerFactory,
controllerReleaser,
propertyBinderFactory,
objectMethodExecutor,
actionMethodExecutor);
cacheEntry = cache.Entries.GetOrAdd(actionDescriptor, cacheEntry);
}
...
return (cacheEntry, filters);
}
於是跟蹤至ControllerBinderDelegateProvider類,找到CreateBinderDelegate方法。
public static ControllerBinderDelegate CreateBinderDelegate(
ParameterBinder parameterBinder,
IModelBinderFactory modelBinderFactory,
IModelMetadataProvider modelMetadataProvider,
ControllerActionDescriptor actionDescriptor)
{
...
var parameterBindingInfo = GetParameterBindingInfo(modelBinderFactory, modelMetadataProvider, actionDescriptor);
...
return 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;
}
}
...
}
}
這裡可以看到創建綁定的delegate方法,與之對應的是之前那句_cacheEntry.ControllerBinderDelegate(_controllerContext, _instance, _arguments)
代碼。
public virtual async Task<ModelBindingResult> BindModelAsync(
ActionContext actionContext,
IModelBinder modelBinder,
IValueProvider valueProvider,
ParameterDescriptor parameter,
ModelMetadata metadata,
object value)
{
...
var modelBindingContext = DefaultModelBindingContext.CreateBindingContext(
actionContext,
valueProvider,
metadata,
parameter.BindingInfo,
parameter.Name);
modelBindingContext.Model = value;
...
await modelBinder.BindModelAsync(modelBindingContext);
...
var modelBindingResult = modelBindingContext.Result;
...
return modelBindingResult;
}
到了此處,就是旅程的終點。ParameterBinder類的BindModelAsync中可以找到對IModelBinder類型的BindModelAsync方法的調用。Model Binding這一操作便是在此時此地實現的。
接下來的疑問有兩處,modelBinder是如何產生的,請求中的數據又是怎樣與modelBinder發生聯繫。
ModelBinder
回到ControllerBinderDelegateProvider類的CreateBinderDelegate方法,可以看到其中調用了GetParameterBindingInfo方法。
private static BinderItem[] GetParameterBindingInfo(
IModelBinderFactory modelBinderFactory,
IModelMetadataProvider modelMetadataProvider,
ControllerActionDescriptor actionDescriptor)
{
var parameters = actionDescriptor.Parameters;
...
var parameterBindingInfo = new BinderItem[parameters.Count];
for (var i = 0; i < parameters.Count; i++)
{
var parameter = parameters[i];
...
var binder = modelBinderFactory.CreateBinder(new ModelBinderFactoryContext
{
BindingInfo = parameter.BindingInfo,
Metadata = metadata,
CacheToken = parameter,
});
parameterBindingInfo[i] = new BinderItem(binder, metadata);
}
return parameterBindingInfo;
}
這裡的代碼很明顯地說明瞭modelBinder由ModelBinderFactory類的CreateBinder方法創建。
public IModelBinder CreateBinder(ModelBinderFactoryContext context)
{
...
IModelBinder binder;
if (TryGetCachedBinder(context.Metadata, context.CacheToken, out binder))
{
return binder;
}
var providerContext = new DefaultModelBinderProviderContext(this, context);
binder = CreateBinderCoreUncached(providerContext, context.CacheToken);
...
AddToCache(context.Metadata, context.CacheToken, binder);
return binder;
}
CreateBinder方法內部中如果緩存可以取到值,則從緩存內取值並直接返回,否則通過CreateBinderCoreUncached方法取值。
private IModelBinder CreateBinderCoreUncached(DefaultModelBinderProviderContext providerContext, object token)
{
...
IModelBinder result = null;
for (var i = 0; i < _providers.Length; i++)
{
var provider = _providers[i];
result = provider.GetBinder(providerContext);
if (result != null)
{
break;
}
}
...
return result;
}
這裡的providers集合又包含哪些數據呢?可以從MvcCoreMvcOptionsSetup類中找到答案。
public void Configure(MvcOptions options)
{
// Set up ModelBinding
options.ModelBinderProviders.Add(new BinderTypeModelBinderProvider());
options.ModelBinderProviders.Add(new ServicesModelBinderProvider());
options.ModelBinderProviders.Add(new BodyModelBinderProvider(options.InputFormatters, _readerFactory, _loggerFactory, options));
options.ModelBinderProviders.Add(new HeaderModelBinderProvider());
options.ModelBinderProviders.Add(new FloatingPointTypeModelBinderProvider());
options.ModelBinderProviders.Add(new EnumTypeModelBinderProvider(options));
options.ModelBinderProviders.Add(new SimpleTypeModelBinderProvider());
options.ModelBinderProviders.Add(new CancellationTokenModelBinderProvider());
options.ModelBinderProviders.Add(new ByteArrayModelBinderProvider());
options.ModelBinderProviders.Add(new FormFileModelBinderProvider());
options.ModelBinderProviders.Add(new FormCollectionModelBinderProvider());
options.ModelBinderProviders.Add(new KeyValuePairModelBinderProvider());
options.ModelBinderProviders.Add(new DictionaryModelBinderProvider());
options.ModelBinderProviders.Add(new ArrayModelBinderProvider());
options.ModelBinderProviders.Add(new CollectionModelBinderProvider());
options.ModelBinderProviders.Add(new ComplexTypeModelBinderProvider());
...
}
以上便是.NET Core MVC中所有被框架支持的ModelBinderProvider。
以一個最典型的FormCollectionModelBinderProvider為例。它以Metadata.ModelType的類型作為判斷依據,如果是IFormCollection類型的話,則返回一個FormCollectionModelBinder對象。
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
...
var modelType = context.Metadata.ModelType;
...
if (modelType == typeof(IFormCollection))
{
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
return new FormCollectionModelBinder(loggerFactory);
}
return null;
}
在CreateBinderCoreUncached方法的迴圈體內部會依次嘗試ModelBinderProvider們是否能創建合適的ModelBinder,一旦能夠生成ModelBinder,則跳出當前迴圈,以這個對象作為返回值。
ValueProvider
有了ModelBinder,還需要有數據才能進行綁定。而為ModelBinder提供數據的是一些ValueProvider。
MvcCoreMvcOptionsSetup類的Configure方法里,再往下找,可以看到ValueProvider們的蹤影。更確切地是與之對應的工廠類們。
public void Configure(MvcOptions options)
{
...
// Set up ValueProviders
options.ValueProviderFactories.Add(new FormValueProviderFactory());
options.ValueProviderFactories.Add(new RouteValueProviderFactory());
options.ValueProviderFactories.Add(new QueryStringValueProviderFactory());
options.ValueProviderFactories.Add(new JQueryFormValueProviderFactory());
...
}
以FormValueProviderFactory為例,看一下其內部:
public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
{
...
var request = context.ActionContext.HttpContext.Request;
if (request.HasFormContentType)
{
// Allocating a Task only when the body is form data.
return AddValueProviderAsync(context);
}
return Task.CompletedTask;
}
private static async Task AddValueProviderAsync(ValueProviderFactoryContext context)
{
var request = context.ActionContext.HttpContext.Request;
var valueProvider = new FormValueProvider(
BindingSource.Form,
await request.ReadFormAsync(),
CultureInfo.CurrentCulture);
context.ValueProviders.Add(valueProvider);
}
通過CreateValueProviderAsync方法可以得到一個FormValueProvider對象。
而這些ValueProviderFactory所創建的ValueProvider又統一被CompositeValueProvider類的CreateAsync方法聚合成CompositeValueProvider這個集合對象的內部元素。
public static async Task<CompositeValueProvider> CreateAsync(
ActionContext actionContext,
IList<IValueProviderFactory> factories)
{
var valueProviderFactoryContext = new ValueProviderFactoryContext(actionContext);
for (var i = 0; i < factories.Count; i++)
{
var factory = factories[i];
await factory.CreateValueProviderAsync(valueProviderFactoryContext);
}
return new CompositeValueProvider(valueProviderFactoryContext.ValueProviders);
}
再到ControllerBinderDelegateProvider類的CreateBinderDelegate方法中,找到valueProvider創建的起始點。
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;
}
}
...
}
所得到的valueProvider在ParameterBinder類的BindModelAsync方法里還要再作進一步的處理。先作為參數傳入創建DefaultModelBindingContext的方法:
var modelBindingContext = DefaultModelBindingContext.CreateBindingContext(
actionContext,
valueProvider,
metadata,
parameter.BindingInfo,
parameter.Name);
再對ValueProvider作過濾處理:
return new DefaultModelBindingContext()
{
ActionContext = actionContext,
BinderModelName = binderModelName,
BindingSource = bindingSource,
PropertyFilter = propertyFilterProvider?.PropertyFilter,
// Because this is the top-level context, FieldName and ModelName should be the same.
FieldName = binderModelName ?? modelName,
ModelName = binderModelName ?? modelName,
IsTopLevelObject = true,
ModelMetadata = metadata,
ModelState = actionContext.ModelState,
OriginalValueProvider = valueProvider,
ValueProvider = FilterValueProvider(valueProvider, bindingSource),
ValidationState = new ValidationStateDictionary(),
};
FilterValueProvider方法最終會調用CompositeValueProvider類的Filter方法,以得到所有合適的valueProvider。
public IValueProvider Filter(BindingSource bindingSource)
{
...
var filteredValueProviders = new List<IValueProvider>();
foreach (var valueProvider in this.OfType<IBindingSourceValueProvider>())
{
var result = valueProvider.Filter(bindingSource);
if (result != null)
{
filteredValueProviders.Add(result);
}
}
...
return new CompositeValueProvider(filteredValueProviders);
}
那麼當在ModelBinder的BindModelAsync方法里需要獲取數據時,以FloatModelBinder為例:
public Task BindModelAsync(ModelBindingContext bindingContext)
{
...
var modelName = bindingContext.ModelName;
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
...
}
會試圖從已過濾的ValueProvider中獲取值。這時還是利用了CompositeValueProvider類中的方法。
public virtual ValueProviderResult GetValue(string key)
{
// Performance-sensitive
// Caching the count is faster for IList<T>
var itemCount = Items.Count;
for (var i = 0; i < itemCount; i++)
{
var valueProvider = Items[i];
var result = valueProvider.GetValue(key);
if (result != ValueProviderResult.None)
{
return result;
}
}
return ValueProviderResult.None;
}
這裡的邏輯是從valueProvider集合中逐一嘗試取值,有數據的則直接返回。
這也意味著數據綁定會以FormValueProvider到RouteValueProvider,再到QueryStringValueProvider,最後向JQueryFormValueProvider取值,這一流程執行,中間如果有任何一個能得到數據的話,則不再繼續訪問後面的ValueProvider。當然,前提是這些ValueProvider要不被先前的過濾處理排除在外。
若是還不明白這一順序關係的話,可以回想下從ValueProviderFactories的添加順序,再至ValueProvider集合生成時各個ValueProvider的順序,就比較容易瞭解其中道理了。