通過極簡模擬框架讓你了解ASP.NET Core MVC框架的設計與實現[下篇]:參數綁定


模擬框架到目前為止都假定Action方法是沒有參數的,我們知道MVC框架對Action方法的參數並沒有作限制,它可以包含任意數量和類型的參數。一旦將“零參數”的假設去除,ControllerActionInvoker針對Action方法的執行就變得沒那么簡單了,因為在執行目標方法之前需要綁定所有的參數。MVC框架采用一種叫做“模型綁定(Model Binding)”的機制來綁定目標Action方法的輸出參數,這可以算是MVC框架針對請求執行流程中最為復雜的一個環節,為了讓讀者朋友們對模型綁定的設計和實現原理有一個大致的了解,模擬框架提供一個極簡版本的實現。源代碼從這里下載。

目錄
一、數據項的提供
     IValueProvider
     IValueProviderFactory
二、模型綁定
     ModelMetada
     IModelBinder
     IModelBinderProvider
     IModelBinderFactory
三、簡單類型綁定
四、復雜類型綁定
     針對屬性成員的遞歸綁定
     針對反序列化的綁定
五、綁定方法的參數
六、實例演示

一、數據項的提供

雖然MVC框架並沒有數據來源作任何限制,但是模型綁定的原始數據一般來源於當前的請求。除了以請求主體的形式提供一段完整的內容(比如JSON或者XML片段)並最終通過發序列化的方式生成作為參數的對象之外,HTTP請求大都會采用鍵值對的形式提供一組候選的數據項作為模型綁定的數據源,比如請求URL提供的查詢字符串(Query String)、請求路徑解析之后得到的路由參數請求的首部集合、主體攜帶的表單元素等。

IValueProvider

作為對這些采用鍵值對結構的原始數據項提供者的抽象,MVC框架提供了一個名為IValueProvider接口,模擬框架對該接口作了如下的簡化。與具有唯一鍵的字典不同,作為模型綁定數據源的多個數據項可以共享同一個名稱,並且它們基本以字符串的形式存在,所以IValueProvider接口定義了一個TryGetValues方法根據指定的名稱得到一組以字符串數組表示的值。我們還為IValueProvider接口定義了一個ContainsPrefix方法來確定是否包含指定名稱前綴的數據項。

public interface IValueProvider
{
    bool TryGetValues(string name, out string[] values);
    bool ContainsPrefix(string prefix);
}

由於數據來源的多樣性,所以一個應用會涉及到針對多個IValueProvider對象的使用。不論模型綁定支持多少種數據源,如果我們總是能夠使用一個單一IValueProvider對象來提供模型綁定的數據項,這無疑會使模型綁定的設計變得更加簡單。對設計模式稍有了解的等着朋友應該會想到“組合模式”會幫我們實現這個目標,為此我們按照這樣的模式定義了如下這個CompositeValueProvider類型。

public class CompositeValueProvider : IValueProvider
{
    private readonly IEnumerable<IValueProvider> _providers;
    public CompositeValueProvider(IEnumerable<IValueProvider> providers) => _providers = providers;
    public bool ContainsPrefix(string prefix) => _providers.Any(it => it.ContainsPrefix(prefix));
    public bool TryGetValues(string name, out string[] value)
    {
        foreach (var provider in _providers)
        {
            if (provider.TryGetValues(name, out value))
            {
                return true;
            }
        }
        return (value = null) != null;
    }
}

如下所示的ValueProvider類型是模擬框架提供的針對IValueProvider接口的模式實現。如代碼片段所示,ValueProvider利用一個NameValueCollection來保存作為數據項的字符串鍵值對,值得一提的是,對於我們提供的這個NameValueCollection對象,它的Key是不區分大小寫的。除了提供一個將NameValueCollection對象作為參數的構造函數之外,我們還提供了另一個構造函數,該構造函數將一個IEnumerable<KeyValuePair<string, StringValues>>對象作為數據的原始來源。

public class ValueProvider : IValueProvider
{
    private readonly NameValueCollection _values; 
    public static ValueProvider Empty = new ValueProvider(new NameValueCollection());

    public ValueProvider(NameValueCollection values) => _values = new NameValueCollection(StringComparer.OrdinalIgnoreCase) { values };

    public ValueProvider(IEnumerable<KeyValuePair<string, StringValues>> values)
    {
        _values = new NameValueCollection(StringComparer.OrdinalIgnoreCase);
        foreach (var kv in values)
        {
            foreach (var value in kv.Value)
            {
                _values.Add(kv.Key.Replace("-", ""), value);
            }
        }
    }

    public bool ContainsPrefix(string prefix)
    {
        foreach (string key in _values.Keys)
        {
            if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }
        }
        return false;
    }

    public bool TryGetValues(string name, out string[] value)
    {
        value = _values.GetValues(name);
        return value?.Any() == true;
    }
}

IValueProviderFactory

顧名思義,IValueProviderFactory接口代表創建IValueProvider對象的工廠。如下面的代碼片段所示,IValueProviderFactory接口定義了唯一的CreateValueProvider方法,該方法根據提供的ActionContext上下文創建出對應的IValueProvider對象。

public interface IValueProviderFactory
{
    IValueProvider CreateValueProvider(ActionContext actionContext);
}

如果我們需要為模型綁定提供針對某項數據源的支持,我們只需要定義和注冊針對IValueProviderFactory接口的實現類即可。如下所示的三個類型(QueryStringValueProviderFactory、HttpHeaderValueProviderFactory和FormValueProviderFactory)會將請求的查詢字符串、首部集合和提交表單作為模型綁定的數據源。

public class QueryStringValueProviderFactory : IValueProviderFactory
{
    public IValueProvider CreateValueProvider(ActionContext actionContext)  => new ValueProvider(actionContext.HttpContext.Request.Query);
}

public class HttpHeaderValueProviderFactory : IValueProviderFactory
{
    public IValueProvider CreateValueProvider(ActionContext actionContext)  => new ValueProvider(actionContext.HttpContext.Request.Headers);
}

public class FormValueProviderFactory : IValueProviderFactory
{
    public IValueProvider CreateValueProvider(ActionContext actionContext)
    {
        var contentType = actionContext.HttpContext.Request.GetTypedHeaders().ContentType;
        return contentType.MediaType.Equals("application/x-www-form-urlencoded")
        ? new ValueProvider(actionContext.HttpContext.Request.Form)
        : ValueProvider.Empty;
    }
}

二、模型綁定

模型綁定最終是通過相應的IModelBinder對象完成的,具體的IModelBinder對象是根據描述待綁定模型的元數據來提供的。模型元數據不僅決定了實施綁定的IModelBinder對象的類型,還為模型綁定提供了必要的元數據。

ModelMetada

模擬框架利用如下這個ModelMetadata對模型元數據進行了極大的簡化。由於模型綁定最終的目的是為了提供Action方法的某個參數值,所以用來控制或者輔助綁定的元數據可以通過描述參數的ParameterInfo對象提取出來。針對復合對象的模型綁定是一個遞歸的過程:先創建一個空的對象,並采用同樣的模型綁定機制去初始化相應的屬性,所以針對該屬性的模型元數據應根據對應的PropertyInfo對象來創建。ModelMetadata提供了兩個靜態工廠方法來完成上述兩種針對ModelMetadata對象的創建。

public class ModelMetadata
{
    public ParameterInfo Parameter { get; }
    public PropertyInfo Property { get; }
    public Type ModelType { get; }
    public bool CanConvertFromString { get; }

    private ModelMetadata(ParameterInfo parameter, PropertyInfo property)
    {
        Parameter = parameter;
        Property = property;
        ModelType = parameter?.ParameterType ?? property.PropertyType;
        CanConvertFromString = TypeDescriptor.GetConverter(ModelType).CanConvertFrom(typeof(string));
    }

    public static ModelMetadata CreateByParameter(ParameterInfo parameter) => new ModelMetadata(parameter, null);

    public static ModelMetadata CreateByProperty(PropertyInfo property) => new ModelMetadata(null, property);
}

ModelMetadata的ModelType屬性表示待綁定的目標類型。按照采用的綁定策略的差異,我們將待綁定的數據類型划分為兩種類型——簡單類型復雜類型。對於一個給定的數據類型,決定它屬於簡單類型還是復雜類型卻決於:是否支持源自字符串類型的類型轉換。之所以采用是否支持源自字符串類型的轉換作為復雜類型和簡單類型的划分依據,是因為IValueProvider提供的原子數據項封裝的原始數據就是一個字符串,如果目標類型支持源自字符串的類型轉換,對應的綁定只需要將原始數據轉換成目標類型就可以了。待綁定數據類型是否支持源自字符串的類型轉換可以通過ModelMetadata類型的CanConvertFromString屬性來決定。

IModelBinder

如下所示的是用於實施模型綁定的IModelBinder接口的定義。如代碼片段所示,IModelBinder接口定義了唯一的方法BindAsync方法在通過參數表示的綁定上下文中完成模型綁定。作為模型綁定上下文的ModelBindingContext對象承載了用來完成模型綁定的輸入和輔助對象,完成綁定得到的模型對象最終也需要附加到此上下文中。

public interface IModelBinder
{
    public Task BindAsync(ModelBindingContext context);
}

被模擬框架簡化后的綁定上下文體現為如下這個ModelBindingContext類型。前面四個屬性(ActionContext、ModelName和ModelMetadata和ValueProvider)是為模型綁定提供的輸入,分別表示當前ActionContext上下文、模型名稱、模型元數據和提供原子數據源的IValueProvider對象,其中表示模型名稱的ModelName為我們提供從IValueProvider對象提取對應數據項的Key。

public class ModelBindingContext
{
    public ActionContext ActionContext { get; }
    public string ModelName { get; }
    public ModelMetadata ModelMetadata { get; }
    public IValueProvider ValueProvider { get; }

    public object Model { get; private set; }
    public bool IsModelSet { get; private set; }

    public ModelBindingContext(ActionContext actionContext, string modelName, ModelMetadata modelMetadata, IValueProvider valueProvider)
    {
        ActionContext = actionContext;
        ModelName = modelName;
        ModelMetadata = modelMetadata;
        ValueProvider = valueProvider;
    }
    public void Bind(object model)
    {
        Model = model;
        IsModelSet = true;
    }
}

ModelBindingContext類型的Model和IsModelSet屬性作為模型綁定的輸出,前者表示綁定生成的目標對象,后者則表示是否綁定的目標對象是否成功生成並賦值到Model屬性上(不能通過Model屬性是否返回Null來決定,因為綁定生成的目標對象可能就是Null)。我們將Model和IsModelSet都定義成私有屬性,因為我們希望通過Bind方法來對它們進行賦值。

IModelBinderProvider

IModelBinder對象由對應的IModelBinderProvider對象來提供。如下所示的是模擬框架對該接口的簡化定義,如代碼片段所示,IModelBinderProvider接口定義了唯一的GetBinder方法根據提供的用於描述待綁定模型元數據的ModelMetadata對象來提供對應的IModelBinder對象。

public interface IModelBinderProvider
{
    IModelBinder GetBinder(ModelMetadata metadata);
}

IModelBinderFactory

一般來說,每個具體的IModelBinder實現類型都具有一個對應的IModelBinderProvider實現類型,所以ASP.NET Core應用采用注冊多個IModelBinderProvider實現類型的方式來提供針對不同模型綁定方式的支持。最終針對IModelBinder對象的提供體現為如何根據待綁定模型元數據選擇正確的IModelBinderProvider對象來提供對應的IModelBinder對象,這一功能是通過IModelBinderFactory對象來完成的。如下面的代碼片段所示,IModelBinderFactory接口定義了唯一的CreateBinder方法根據提供的模型元數據來創建對應的IModelBinder對象。

public interface IModelBinderFactory
{
    IModelBinder CreateBinder(ModelMetadata metadata);
}

如下所示的ModelBinderFactory類型是模擬框架提供的針對IModelBinderFactory接口的默認實現。一個ModelBinderFactory對象是對一組IModelBinderProvider對象的封裝,在實現的CreateBinder方法中,它通過遍歷這組IModelBinderProvider對象,並返回第一個提供的IModelBinder對象。

public class ModelBinderFactory : IModelBinderFactory
{
    private readonly IEnumerable<IModelBinderProvider> _providers;

    public ModelBinderFactory(IEnumerable<IModelBinderProvider> providers) => _providers = providers;

    public IModelBinder CreateBinder(ModelMetadata metadata)
    {
        foreach (var provider in _providers)
        {
            var binder = provider.GetBinder(metadata);
            if (binder != null)
            {
                return binder;
            }
        }
        return null;
    }
}

三、簡單類型綁定

雖然真正的MVC框架支持包括數組、集合和字典類型的大部分數據類型的綁定,但我們的模擬框架只關注單純的簡單類型(Simple Type)和復雜類型(Complex Type)的綁定,不支持針對數組、集合和字典等類型的綁定。針對簡單類型的模型綁定實現在如下這個SimpleModelBinder類型中。

public class SimpleTypeModelBinder : IModelBinder
{
    public Task BindAsync(ModelBindingContext context)
    {
        if (context.ValueProvider.TryGetValues(context.ModelName, out var values))
        {
            var model = Convert.ChangeType(values.Last(), context.ModelMetadata.ModelType);
            context.Bind(model);
        }
        return Task.CompletedTask;
    }
}

如上面的代碼片段所示,在實現的BindAsync方法中,我們從表示綁定上下文的ModelBindingContext對象中得到用來提供原子數據項的IValueProvider對象,並將ModelName屬性表示的模型名稱作為參數調用該對象的TryGetValues方法。如果成功獲得對應的數據項,我們只需要將以字符串形式表示的原始值(如果有多個,取最后一個)直接轉換成目標類型,並調用ModelBindingContext上下文的Bind方法完成綁定即可。

如下所示的SimpleTypeModelBinderProvider是SimpleTypeModelBinder對應的IModelBinderProvider實現類型。如果代碼片段所示,在實現的GetBinder方法中,如果通過提供的模型元數據判斷待綁定的目標類型支持源自字符串的類型轉換,它會直接返回一個創建的SimpleTypeModelBinder對象,否則方法會返回Null。

public class SimpleTypeModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelMetadata metadata) => metadata.CanConvertFromString ? new SimpleTypeModelBinder() : null;
}

四、復雜類型綁定

一般來說,模型綁定的復雜類型就是具有屬性成員的復合類型(如果我們為該類型定義了源自字符串類型的TypeConverter,該類型會變成簡單類型)。針對復雜類型的綁定主要有兩種形式,一種先是創建一個空對象並以遞歸的形式綁定其屬性成員,另一種是直接提取請求主體承載的內容(比如JSON或者XML片段)采用反序列化的方式生成目標對象。

針對屬性成員的遞歸綁定

如果采用針對屬性成員的遞歸綁定方式,綁定的目標對象實際上是通過IValueProvider對象提供的多個原子數據項組合而成,那么先擇需要解決的是原子數據項的名稱與復雜數據對象的屬性成員的映射關系。如果將屬性表示成一條分支,任何一個復合對象都可以描述成一棵樹,這棵樹的葉子節點均為支持源自字符串類型轉換的簡單類型。要綁定為一個復雜對象,需要提供綁定為葉子節點所需的數據項。由於每個葉子節點的路徑具有唯一性,如果將此路徑來命名數據項,那么數據項與葉子節點就能對應起來。

public class Foobarbaz
{ 
    public Foobar Foobar { get; set; }
    public double Baz { get; set; }
}

public class Foobar
{
    public string  Foo { get; set; }
    public int  Bar { get; set; }
}

舉一個例子,假設綁定的目標類型為具有如上定義的Foobarbaz,它定義了兩個屬性Foobar和Baz。Baz屬性的類型為double,所以是一個簡單類型。Foobar屬性為復雜類型Foobar,又包含兩個簡單類型的屬性(Foo和Bar)。那么一個Foobarbaz對象可以表示為一棵如下圖所示的樹。

5-4

如果我們利用一個IValueProvider對象來提供一個完整的Foobarbaz對象,只需要提供綁定三個葉子節點所需的數據項,我們可以采用如下所示的方式利用葉子節點的路徑作為數據項的名稱。

Key        Value
-------------------
Foobar.Foo    123
Foobar.Bar    456
Baz           789

如果目標Action方法具有兩個類型均為Foobarbaz的參數(value1和value2),如果IValueProvider對象只提供上述的數據項,那么綁定的兩個參數將承載完全相同的數據。如果對具體的參數進行針對性的綁定,可以將采用如下的方式以參數名作為前綴。

Key            Value
-------------------------
value1.Foobar.Foo    111
value1.Foobar.Bar    111
value1.Baz           111

value2.Foobar.Foo    222
value2.Foobar.Bar    222
value2.Baz           222

針對復雜類型的第一種模型綁定方式通過如下這個ComplexTypeModelBinder類型來完成。正如前面提到過的,在實現的BindAsync方法中,ComplexTypeModelBinder對象會從模型元數據中得到待綁定的目標類型,並通過反射的方式創建一個空的對象。接下來,它會遍歷每一個支持賦值的屬性,並遞歸地采用模型綁定得到對應屬性值,並對屬性予以賦值。BindAsync最終會將之前創建的對象作為綁定的目標對象。

public class ComplexTypeModelBinder : IModelBinder
{
    public async Task BindAsync(ModelBindingContext context)
    {
        var metadata = context.ModelMetadata;
        var model = Activator.CreateInstance(metadata.ModelType);
        foreach (var property in metadata.ModelType.GetProperties().Where(it => it.SetMethod != null))
        {
            var binderFactory = context.ActionContext.HttpContext.RequestServices.GetRequiredService<IModelBinderFactory>();
            var propertyMetadata = ModelMetadata.CreateByProperty(property);
            var binder = binderFactory.CreateBinder(propertyMetadata);
            var modelName = string.IsNullOrWhiteSpace(context.ModelName)
                ? property.Name
                : $"{context.ModelName}.{property.Name}";
            var propertyContext = new ModelBindingContext(context.ActionContext, modelName, propertyMetadata, context.ValueProvider);
            await binder.BindAsync(propertyContext);
            if (propertyContext.IsModelSet)
            {
                property.SetValue(model, propertyContext.Model);
            }
        }
        context.Bind(model);
    }
}

在針對每個屬性的模型綁定實施過程中,ComplexTypeModelBinder對象會利用針對當前請求的IServiceProvider對象得到注冊的IModelBinderFactory對象,然后根據當前屬性創建創建一個描述模型元數據的ModelMetadata對象,並將其作為參數調用IModelBinderFactory對象的CreateBinder方法得到用來綁定當前屬性的IModelBinder對象。ComplexTypeModelBinder隨后會創建作為綁定上下文的ModelBindingContext對象,當前上下文的ModelName屬性附加上當前屬性名之后會作為新創建上下文的ModelName屬性。ComplexTypeModelBinder最后會將此上下文作為參數調用IModelBinder對象的BindAsync方法完成針對當前屬性的模型綁定。

ComplexTypeModelBinder將作為復雜類型的默認IModelBinder類型。如果希望采用基於反序列化的綁定方式,模擬框架假設對應的參數上會顯式標注FromBodyAttribute特性。所以ComplexTypeModelBinderProvider會采用如下的方式來提供ComplexTypeModelBinder對象。

public class ComplexTypeModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelMetadata metadata)
    {
        if (metadata.CanConvertFromString)
        {
            return null;
        }
        return metadata.Parameter?.GetCustomAttribute<FromBodyAttribute>() == null
            ? new ComplexTypeModelBinder()
            : null;
    }
}

針對反序列化的綁定

如果希望通過反序列化請求主體內容的方式來綁定復雜類型參數,我們可以采用如下這個BodyModelBinder類型。簡單起見,在實現的BindAsync方法中,我們只實現了針對JSON的反序列化。BodyModelBinder對象由如下所示的BodyModelBinderProvider類型提供。

public class BodyModelBinder : IModelBinder
{
    public async Task BindAsync(ModelBindingContext context)
    {
        var input = context.ActionContext.HttpContext.Request.Body;
        var result = await JsonSerializer.DeserializeAsync(input, context.ModelMetadata.ModelType);
        context.Bind(result);
    }
}

public class BodyModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelMetadata metadata)
    {
        return metadata.Parameter?.GetCustomAttribute<FromBodyAttribute>() == null
            ? null
            : new BodyModelBinder();                
    }
}

五、綁定方法的參數

當目前位置,我們已經完成了所有模型綁定的所需的工作,接下來我們將基於模型綁定的參數綁定實現在ControllerActionInvoker之中,為此我們定義了在該類型中定義了如下這個BindArgumentsAsync方法,該方法會返回指定Action方法的參數列表。如下面的代碼片段所示,如果目標方法沒有參數,BindArgumentsAsync方法只需要返回一個空的對象數組。

public class ControllerActionInvoker : IActionInvoker
{   
    private async Task<object[]> BindArgumentsAsync(MethodInfo methodInfo)
    {
        var parameters = methodInfo.GetParameters();
        if (parameters.Length == 0)
        {
            return new object[0];
        }
        var arguments = new object[parameters.Length];
        for (int index = 0; index < arguments.Length; index++)
        {
            var parameter = parameters[index];
            var metadata = ModelMetadata.CreateByParameter(parameter);
            var requestServices = ActionContext.HttpContext.RequestServices;
            var valueProviderFactories = requestServices.GetServices<IValueProviderFactory>();
            var valueProvider = new CompositeValueProvider(valueProviderFactories.Select(it => it.CreateValueProvider(ActionContext))); 
            var modelBinderFactory = requestServices.GetRequiredService<IModelBinderFactory>();
            var context = valueProvider.ContainsPrefix(parameter.Name)
                ? new ModelBindingContext(ActionContext, parameter.Name, metadata, valueProvider)
                : new ModelBindingContext(ActionContext, "", metadata, valueProvider);
            var binder = modelBinderFactory.CreateBinder(metadata);
            await binder.BindAsync(context);
            arguments[index] = context.Model;
        }
        return arguments;
    }
    …
}

如果目標Action方法定義了參數,BindArgumentsAsync方法會為針對每個參數采用模型綁定的方式得到對應的參數值。具體來說,BindArgumentsAsync方法會利用根據當前參數創建描述目標模型元數據的ModelMetadata對象。接下來,該方法針對當前請求的IServiceProvider對象得到當前注冊的所有IValueProviderFactory對象,並利用它們提供的IValueProvider對象創建一個CompositeValueProvider對象。接下來,該方法再次利用同一個IServiceProvider對象得到注冊的IModelBinderFactory對象,並利用它根據模型元數據得到實施模型綁定的IModelBinder對象。

BindArgumentsAsync方法會根據當前的ActionContext上下文和預先創建的ModelMetadata對象、CompositeValueProvider對象創建出代表綁定上下文的ModelBindingContext對象。如果CompositeValueProvider對象能夠提供參數名稱作為名稱前綴的數據項,那么參數名稱將作為ModelBindingContext對象的ModelName屬性,否則該屬性將設置為空字符串。針對ModelName屬性的命名規則確保數據源通過將參數名稱作為前綴實現針對具體某個參數的綁定,也可以不用指定這個前綴綁定所有參數。BindArgumentsAsync最終將這個綁定上下文作為調用IModelBinder對象的BindAsync方法,並通過上下文的Model屬性得到綁定的參數值。

public class ControllerActionInvoker : IActionInvoker
{    
    public async Task InvokeAsync()
    {
        var actionDescriptor =  (ControllerActionDescriptor)ActionContext.ActionDescriptor;
        var controllerType = actionDescriptor.ControllerType;
        var requestServies = ActionContext.HttpContext.RequestServices;
        var controllerInstance = ActivatorUtilities.CreateInstance(requestServies, controllerType);
        if (controllerInstance is Controller controller)
        {
            controller.ActionContext = ActionContext;
        }
        var actionMethod = actionDescriptor.Method;
        var arguments = await BindArgumentsAsync(actionMethod); var returnValue = actionMethod.Invoke(controllerInstance, arguments);
        var mapper = requestServies.GetRequiredService<IActionResultTypeMapper>();
        var actionResult = await ToActionResultAsync(returnValue, actionMethod.ReturnType, mapper);
        await actionResult.ExecuteResultAsync(ActionContext);
    }
}

在用於執行目標Action的InvokeAsync方法中,我們在通過描述Action的ControllerActionDescriptor對象得到表示目標Action方法的MethodInfo對象之后,我們將其作為參數調用了上面定義的BindArgumentsAsync方法得到待執行方法的參數列表,並最終以反射的方式執行目標Action方法。

由於針對模型綁定的所有服務對象都是利用依賴注入容器獲取的,所以我們需要作相應的服務注冊。在前面定義的針對IServiceCollection接口的AddMvcControllers擴展方法中,我們采用如下的方式分別完成了針對IValueProviderFactory、IModelBinderFactory和IModelBinderProvider的服務注冊。

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMvcControllers(this IServiceCollection services)
    {
        return services
            .AddSingleton<IActionDescriptorCollectionProvider, DefaultActionDescriptorCollectionProvider>()
            .AddSingleton<IActionInvokerFactory, ActionInvokerFactory>()
            .AddSingleton<IActionDescriptorProvider, ControllerActionDescriptorProvider>()
            .AddSingleton<ControllerActionEndpointDataSource,ControllerActionEndpointDataSource>()
            .AddSingleton<IActionResultTypeMapper, ActionResultTypeMapper>()

          .AddSingleton<IValueProviderFactory, HttpHeaderValueProviderFactory>() .AddSingleton<IValueProviderFactory, QueryStringValueProviderFactory>() .AddSingleton<IValueProviderFactory, FormValueProviderFactory>() .AddSingleton<IModelBinderFactory, ModelBinderFactory>() .AddSingleton<IModelBinderProvider, SimpleTypeModelBinderProvider>() .AddSingleton<IModelBinderProvider, ComplexTypeModelBinderProvider>() .AddSingleton<IModelBinderProvider, BodyModelBinderProvider>();
    }
}

六、實例演示

為了演示ControllerActionInvoker基於模型綁定的參數綁定機制,我們在前面演示的應用程序中定義了如下這個HomeController類型。我們在該Controller類型中定義了三個返回類型為字符串的Action方法(Action1、Action2和Action3)。

public class HomeController
{
    private static readonly JsonSerializerOptions _options = new JsonSerializerOptions { WriteIndented = true };
    public string Action1(string foo, int bar, double baz) => JsonSerializer.Serialize(new { Foo = foo, Bar = bar, Baz = baz }, _options);
    public string Action2(Foobarbaz value1, Foobarbaz value2) => JsonSerializer.Serialize(new { Value1 = value1, Value2 = value2 }, _options);
    public string Action3(Foobarbaz value1, [FromBody]Foobarbaz value2) => JsonSerializer.Serialize(new { Value1 = value1, Value2 = value2 }, _options);
}

Action1方法定義了三個簡單類型的參數(foo、bar和baz),而Action2和 Action3方法定義了Foobarbaz類型(具有如下定義)參數(value1和value2),其中Action3方法的value2參數上標注了FromBodyAttribute特性。為了三個Action方法的輸入參數是否正常綁定,我們將它們組合成一個元組,元組序列化生成的JSON字符串作為方法的返回值。

public class Foobarbaz
{
    public Foobar Foobar { get; set; }
    public double Baz { get; set; }
}

public class Foobar
{
    public string Foo { get; set; }
    public int  Bar { get; set; }
}

我們先來驗證針對Action1方法的參數綁定。由於上面定義的針對IServiceCollection接口的AddMvcControllers擴展方法中注冊了三種針對IValueProviderFactory接口的實現類型(QueryStringValueProviderFactory、HttpHeaderValueProviderFactory和FormValueProviderFactory),意味着我們可以分別采用請求的查詢字符串、首部集合和提交的表單來提供待綁定參數的數據。為了驗證這三種不同的數據來源,我們利用Fiddler針對Action1(/home/action1)發送了三個請求,從返回的響應可以看出該方法的三個參數均綁定了正確的數值。

GET http://localhost:5000/home/action1?foo=123&bar=456&baz=789 HTTP/1.1
User-Agent: Fiddler
Host: localhost:5000

HTTP/1.1 200 OK
Date: Fri, 14 Feb 2020 02:14:58 GMT
Content-Type: text/plain
Server: Kestrel
Content-Length: 50

{
  "Foo": "123",
  "Bar": 456,
  "Baz": 789
}


GET http://localhost:5000/home/action1 HTTP/1.1
Foo: 123
Bar: 456
Baz: 789
Host: localhost:5000

HTTP/1.1 200 OK
Date: Fri, 14 Feb 2020 02:17:41 GMT
Content-Type: text/plain
Server: Kestrel
Content-Length: 50

{
  "Foo": "123",
  "Bar": 456,
  "Baz": 789
}


POST http://localhost:5000/home/action1 HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Host: localhost:5000
Content-Length: 23

foo=123&bar=456&baz=789

HTTP/1.1 200 OK
Date: Fri, 14 Feb 2020 02:19:18 GMT
Content-Type: text/plain
Server: Kestrel
Content-Length: 50

{
  "Foo": "123",
  "Bar": 456,
  "Baz": 789
}

對於Action2方法來說,由於兩個參數的類型Foobarbaz為復雜類型,默認會采用遞歸的模型綁定方式來生成對應的參數值。我們同樣采用Fiddler發送了兩組針對該Action方法(/home/action2)的POST請求,並利用提交的表單來提供原始的數據項,表單元素采用上面所述的命名方式。由於第一個請求提交的表單元素沒有采用參數名作為前綴,所以兩個參數最終綁定了相同的數據。第二個請求提交了兩組以參數名前綴命名的表單元素,它們會分別綁定到各自的參數上。(S504)

POST http://localhost:5000/home/action2 HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Host: localhost:5000
Content-Length: 37

foobar.foo=123&foobar.bar=456&baz=789

HTTP/1.1 200 OK
Date: Fri, 14 Feb 2020 02:36:20 GMT
Content-Type: text/plain
Server: Kestrel
Content-Length: 205

{
  "Value1": {
    "Foobar": {
      "Foo": "123",
      "Bar": 456
    },
    "Baz": 789
  },
  "Value2": {
    "Foobar": {
      "Foo": "123",
      "Bar": 456
    },
    "Baz": 789
  }
}


POST http://localhost:5000/home/action2 HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Host: localhost:5000
Content-Length: 117

value1.foobar.foo=111&value1.foobar.bar=222&value1.baz=333&value2.foobar.foo=444&value2.foobar.bar=555&value2.baz=666

HTTP/1.1 200 OK
Date: Fri, 14 Feb 2020 02:37:41 GMT
Content-Type: text/plain
Server: Kestrel
Content-Length: 205

{
  "Value1": {
    "Foobar": {
      "Foo": "111",
      "Bar": 222
    },
    "Baz": 333
  },
  "Value2": {
    "Foobar": {
      "Foo": "444",
      "Bar": 555
    },
    "Baz": 666
  }
}

Action3方法與Action2方法唯一的不同之處在於其第二個參數value2上標注了FromBodyAttribute特性,按照模擬框架的約定,我們會采用基於反序列化(JSON)請求主體內容的方式來綁定該參數。在如下這個針對該Action方法(/home/action3)的請求中,我們以請求首部的方式提供了綁定第一個參數(value1)的數據項,請求主體承載的JSON片段將被反序列化以生成第二個參數(value1)。

POST http://localhost:5000/home/action3 HTTP/1.1
Content-Type: application/json
Foobar.Foo: 111
Foobar.Bar: 222
Baz: 333
Host: localhost:5000
Content-Length: 88

{
    "Foobar": {
      "Foo": "444",
      "Bar": 555
    },
    "Baz": 666
  }

HTTP/1.1 200 OK
Date: Fri, 14 Feb 2020 03:03:54 GMT
Content-Type: text/plain
Server: Kestrel
Content-Length: 205

{
  "Value1": {
    "Foobar": {
      "Foo": "111",
      "Bar": 222
    },
    "Baz": 333
  },
  "Value2": {
    "Foobar": {
      "Foo": "444",
      "Bar": 555
    },
    "Baz": 666
  }
}

通過極簡模擬框架讓你了解ASP.NET Core MVC框架的設計與實現[上篇]:路由整合
通過極簡模擬框架讓你了解ASP.NET Core MVC框架的設計與實現[中篇]: 請求響應
通過極簡模擬框架讓你了解ASP.NET Core MVC框架的設計與實現[下篇]:參數綁定


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM