.NET Core開發日誌——Model Binding

來源:互聯網
上載者:User

標籤:dna   break   obj   content   mat   float   dex   nsset   tps   

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的順序,就比較容易瞭解其中道理了。

.NET Core開發日誌——Model Binding

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.