通过极简模拟框架让你了解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框架的设计与实现[下篇]:参数绑定

posted @ 2020-03-31 08:41  Artech  阅读(1866)  评论(2编辑  收藏