首页后端开发ASP.NET模拟ASP.NET Core MVC设计与实现

模拟ASP.NET Core MVC设计与实现

时间2023-12-07 14:04:59发布访客分类ASP.NET浏览1686
导读:前几天有人在我的《ASP.NET Core框架揭秘》读者群跟我留言说:“我最近在看ASP.NET Core MVC的源代码,发现整个系统太复杂,涉及的东西太多,完全找不到方向,你能不能按照《200行代码,7个对象——让你了解ASP.NET...

前几天有人在我的《ASP.NET Core框架揭秘》读者群跟我留言说:“我最近在看ASP.NET Core MVC的源代码,发现整个系统太复杂,涉及的东西太多,完全找不到方向,你能不能按照《200行代码,7个对象——让你了解ASP.NET Core框架的本质》这篇文章思路剖析一下MVC框架”。对于ASP.NET Core MVC框架的涉及和实现,说难也难,毕竟一个Model Binding就够很多人啃很久,其实说简单也简单,因为整个流程是很清晰的。ASP.NET Core MVC支持基于Controller和Page的两种编程模式,虽然编程方式看起来不太一样,底层针对请求的处理流程其实是一致的。接下来,我同样使用简单的代码构建一个Mini版的MVC框架,让大家了解一下ASP.NET Core MVC背后的总体设计,以及针对请求的处理流程。[源代码从这里下载]。

一、描述Action方法 二、注册路由终结点 三、绑定Action方法参数 四、执行Action方法 五、响应执行结果 六、编排整个处理流程 七、跑起来看看

一、描述Action方法

MVC应用提供的功能体现在一个个Action方法上,所以MVC框架定义了专门的类型ActionDescriptor来描述每个有效的Action方法。但是Action方法和ActionDescriptor对象并非一对一的关系,而是一对多的关系。具体来说,采用“约定路由”的Action方法对应一个ActionDescriptor对象,如果采用“特性路由”,MVC框架会针对每个注册的路由创建一个ActionDescriptor。Action方法与ActionDescriptor之间的映射关系可以通过如下这个演示实例来验证。如代码片段所示,我们调用MapControllerRoute扩展方法注册了4个“约定路由”。HomeController类中定义了两个合法的Action方法,其中方法Foo采用“约定路由”,而方法Bar通过标注的两个HttpGetAttribute特性注册了两个“特性路由”。按照上述的规则,将有三个ActionDescriptor被创建出来,方法Foo有一个,而方法Bar有两个。

var builder = WebApplication.CreateBuilder(args);
    
builder.Services.AddControllers();
    
var app = builder.Build();
    
app.MapControllers();

app.MapControllerRoute("v1", "v1/{
controller}
/{
action}
    ");

app.MapControllerRoute("v2", "v2/{
controller}
/{
action}
    ");

app.MapControllerRoute("v3", "v2/{
controllerx}
/{
action}
    ");

app.MapControllerRoute("v3", "v4/{
controller}
/{
actionx}
    ");
    

app.MapGet("/actions", (IActionDescriptorCollectionProvider provider) =>
 {
    
    var actions = provider.ActionDescriptors.Items;
    
    var builder = new StringBuilder();
    
    foreach (var action in actions.OfTypeControllerActionDescriptor>
())
    {

        builder.AppendLine($"{
action.ControllerTypeInfo.Name}
.{
action.MethodInfo.Name}
({
action.AttributeRouteInfo?.Template ?? "N/A"}
    )");

    }
    
    return builder.ToString();

}
    );
    

app.Run("http://localhost:5000");


public class HomeController
{
    
    public string Foo() =>
 $"{
nameof(HomeController)}
.{
nameof(Foo)}
    ";
    

    [HttpGet("home/bar1")]
    [HttpGet("home/bar2")]
    public string Bar() =>
 $"{
nameof(HomeController)}
.{
nameof(Bar)}
    ";

}

我们注册了一个指向路径“/actions”的路由终结点将所有ActionDescriptor列出来。如代码片段所示,路由处理委托(Lambda表达式)注入了IActionDescriptorCollectionProvider 对象,我们利用它的ActionDescriptors属性得到当前应用承载的所有ActionDescriptor对象。我们将其转化成ControllerActionDescriptor(派生于ActionDescriptor,用于描述定义在Controller类型中的Action方法,另一个派生类PageActionDescriptor用于描述定义在Page类型的Action方法),并将对应的Controller类型和方法名称,以及特性路由模板输出来。如下所示的输出结果验证了上述针对Action方法与ActionDescriptor映射关系的论述。

imageimage

在模拟框架中,我们ActionDescriptor类型作最大的简化。如代码片段所示,创建一个ActionDescriptor对象时只需提供描述目标Action方法的MethodInfo对象(必需),和一个用来定义特性路由的IRouteTemplateProvider对象(可选,仅针对特性路由)。我们利用MethodInfo的声明类型得到Controller的类型,将剔除“Controller”后缀的类型名称作为ControllerName属性(表示Controller的名称),作为Action名称的ActionName属性则直接返回方法名称。Parameters属性返回一个ParameterDescriptor数组,而根据ParameterInfo对象构建的ParameterDescriptor是对参数的描述。

public class ActionDescriptor
{

    public MethodInfo MethodInfo {
     get;
 }

    public IRouteTemplateProvider? RouteTemplateProvider {
     get;
 }

    public string ControllerName {
     get;
 }

    public string ActionName {
     get;
 }

    public ParameterDescriptor[] Parameters {
     get;
 }

    public ActionDescriptor(MethodInfo methodInfo, IRouteTemplateProvider? routeTemplateProvider)
    {
    
        MethodInfo = methodInfo;
    
        RouteTemplateProvider = routeTemplateProvider;
    
        ControllerName = MethodInfo.DeclaringType!.Name;
    
        ControllerName = ControllerName[..^"Controller".Length];
    
        ActionName = MethodInfo.Name;
    
        Parameters = methodInfo.GetParameters().Select(it =>
     new ParameterDescriptor(it)).ToArray();

    }

}


public class ParameterDescriptor(ParameterInfo parameterInfo)
{
    
    public ParameterInfo ParameterInfo =>
     parameterInfo;

}

当前应用涉及的所有ActionActionDescriptor由IActionDescriptorCollectionProvider对象的ActionDescriptors属性来提供。实现类型ActionDescriptorCollectionProvider 从当前启动程序集中提取有效的Controller类型,并将定义其中的有效Action方法转换成ActionDescriptor对象。用于定义“特性路由”的IRouteTemplateProvider对象来源于标注到方法上的特性(简单起见,我们忽略了标注到Controller类型上的特性),比如HttpGetAttribute特性等,同一个Action方法针对注册的特性路由来创建ActionDescriptor就体现在这里。

public interface IActionDescriptorCollectionProvider
{
    
    IReadOnlyListActionDescriptor>
 ActionDescriptors {
     get;
 }

}


public class ActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
{
    
    private readonly Assembly _assembly;
    
    private ListActionDescriptor>
    ? _actionDescriptors;
    
    public IReadOnlyListActionDescriptor>
     ActionDescriptors =>
     _actionDescriptors ??= Resolve(_assembly.GetExportedTypes()).ToList();


    public ActionDescriptorCollectionProvider(IWebHostEnvironment environment)
    {
    
        var assemblyName = new AssemblyName(environment.ApplicationName);
    
        _assembly = Assembly.Load(assemblyName);

    }
    

    private IEnumerableActionDescriptor>
     Resolve(IEnumerableType>
 types)
    {
    
        var methods = types
            .Where(IsValidController)
            .SelectMany(type =>
     type.GetMethods()
            .Where(method =>
     method.DeclaringType == type &
    &
     IsValidAction(method)));


        foreach (var method in methods)
        {
    
            var providers = method.GetCustomAttributes().OfTypeIRouteTemplateProvider>
    ();

            if (providers.Any())
            {

                foreach (var provider in providers)
                {
    
                    yield return new ActionDescriptor(method, provider);

                }

            }

            else
            {
    
                yield return new ActionDescriptor(method, null);

            }

        }

    }
    

    private static bool IsValidController(Type candidate) =>
     candidate.IsPublic &
    &
     !candidate.IsAbstract &
    &
     candidate.Name.EndsWith("Controller");
    
    private static bool IsValidAction(MethodInfo methodInfo) =>
     methodInfo.IsPublic | !methodInfo.IsAbstract;

}
    

二、注册路由终结点

MVC利用“路由”对外提供服务,它会将每个ActionDescriptor转换成“零到多个”路由终结点。ActionDescriptor与终结点之间的对应关系为什么是“零到多”,而不是“一对一”或者“一对多”呢?这也与Action方法采用的路由默认有关,采用特性路由的ActionDescriptor(RouteTemplateProvider 属性不等于Null)总是对应着一个确定的路由,但是如何为采用“约定路由”的ActionDescriptor创建对应的终结点,则取决于多少个约定路由与之匹配。针对每一个基于“约定”路由的ActionDescriptor,系统会为每个与之匹配的路由创建对应的终结点。如果没有匹配的约定路由,对应的Action方法自然就不会有对应的终结点。

我还是利用上面演示实例来说明ActionDescriptor与路由终结点之间的映射关系。为此我们注册如下这个指向路径“/endpoints”的路由终结点,我们通过注入的EndpointDataSource 对象得到终结点列表。由于针对某个Action方法创建的路由终结点都会将ActionDescriptor对象作为元数据,所以我们试着将它(具体类型为ControllerActionDescriptor)提取出来,并输出Controller类型和Action方法的名称,以及路由模板。

...
app.MapGet("/endpoints", (EndpointDataSource source) =>

{
    
    var builder = new StringBuilder();
    
    foreach (var endpoint in source.Endpoints.OfTypeRouteEndpoint>
())
    {
    
        var action = endpoint.Metadata.GetMetadataControllerActionDescriptor>
    ();

        if (action is not null)
        {

            builder.AppendLine($"{
action.ControllerTypeInfo.Name}
.{
action.MethodInfo.Name}
({
endpoint.RoutePattern.RawText}
    )");

        }

    }
    
    return builder.ToString();

}
    );

...

从如下所示的输出结果可以看出,由于Action方法Bar采用“特性路由”,所以对应的ActionDescriptor分别对应着一个终结点。采用约定路由的Foo方法虽然只有一个ActionDescriptor,但是注册的4个约定路由有两个与它匹配(两个必要的路由参数“controller”和“action”需要定义在路由模板中),所以它也具有两个终结点。

imageimage

接下来我们在模拟框架中以最简单的方式完成“路由注册”。我们知道每个路由终结点由“路由模式”和“路由处理器”这两个核心元素构成,前者对应一个RoutePattern对象,由注册的路由信息构建而成,后者体现为一个用来处理请求的RequestDelegate委托。一个MVC应用绝大部分的请求处理工作都落在IActionInvoker对象上,所以作为路由处理器的RequestDelegate委托只需要将请求处理任务“移交”给这个对象就可以了。如代码片段所示,IActionInvoker接口定义了一个无参、返回类型为Task的InvokeAsync方法。IActionInvoker不是一个单例对象,而是针对每个请求单独创建的,创建它的工厂由IActionInvokerFactory接口表示。如代码片段所示,定义在该接口的工厂方法CreateInvoker利用指定的ActionContext上下文来创建返回的IActionInvoker对象。ActionContext可以视为MVC应用的请求上下文,我们的模拟框架同样对它做了最大的简化,将它定义对HttpContext上下文和ActionDescriptor对象的封装。

public interface IActionInvoker
{
    
    Task InvokeAsync();

}


public interface IActionInvokerFactory
{
    
    IActionInvoker CreateInvoker(ActionContext actionContext);

}


public class ActionContext(HttpContext httpContext, ActionDescriptor actionDescriptor)
{
    
    public HttpContext HttpContext =>
     httpContext;
    
    public ActionDescriptor ActionDescriptor =>
     actionDescriptor;

}

我们将路由(终结点)注册实现在一个派生自EndpointDataSource的ActionEndpointDataSource类型中 。对于注册的每个终结点,作为处理器的RequestDelegate委托指向HandleAsync方法,可以看出这个方法的定义非常简单:它从当前终结点中以元数据的形式将ActionDescriptor对象,然后利用它与当前HttpContext将ActionContext上下文创建出来。我们将此ActionContext上下文传递给IActionInvokerFactory工厂将IActionInvoker对象创建出来,并利用它完成后续的请求处理。

public class ActionEndpointDataSource : EndpointDataSource
{
       ...
    private static Task HandleRequestAsync(HttpContext httpContext)
    {
    
        var endpoint = httpContext.GetEndpoint() ?? throw new InvalidOperationException("No endpoint is matched to the current request.");
    
        var actionDescriptor = endpoint.Metadata.GetMetadataActionDescriptor>
    () ?? throw new InvalidOperationException("No ActionDescriptor is attached to the endpoint as metadata.");
    
        var actionContext = new ActionContext(httpContext, actionDescriptor);
    
        return httpContext.RequestServices.GetRequiredServiceIActionInvokerFactory>
    ().CreateInvoker(actionContext).InvokeAsync();

    }

}

ActionEndpointDataSource 定义了一个AddRoute方法来定义约定路由,注册的约定路由被存储在字段_conventionalRoutes所示的列表中。该方法返回一个EndpointConventionBuilder 对象,后者实现了IEndpointConventionBuilder 接口,我们可以利用它对添加的约定约定路由作进一步设置(比如添加元数据)。

public class ActionEndpointDataSource : EndpointDataSource
{
    

    private readonly List(string RouteName, string Template, RouteValueDictionary? Defaults, IDictionarystring, object?>
    ? Constraints, RouteValueDictionary? DataTokens, ListActionEndpointBuilder>
    >
     Conventions, ListActionEndpointBuilder>
    >
     FinallyConventions)>
     _conventionalRoutes = new();
    

    public IEndpointConventionBuilder AddRoute(string routeName, string pattern, RouteValueDictionary? defaults, IDictionarystring, object?>
? constraints, RouteValueDictionary? dataTokens)
    {
    
        var conventions = new ListActionEndpointBuilder>
    >
    ();
    
        var finallyConventions = new ListActionEndpointBuilder>
    >
    ();
    
        _conventionalRoutes.Add((routeName, pattern, defaults, constraints, dataTokens, conventions, finallyConventions));
    
        return new EndpointConventionBuilder(conventions, finallyConventions);

    }

    private sealed class EndpointConventionBuilder : IEndpointConventionBuilder
    {
    
        private readonly ListActionEndpointBuilder>
    >
     _conventions;
    
        private readonly ListActionEndpointBuilder>
    >
     _finallyConventions;
    

        public EndpointConventionBuilder(ListActionEndpointBuilder>
    >
     conventions, ListActionEndpointBuilder>
    >
 finallyConventions)
        {
    
            _conventions = conventions;
    
            _finallyConventions = finallyConventions;

        }
    

        public void Add(ActionEndpointBuilder>
     convention) =>
     _conventions.Add(convention);
    
        public void Finally(ActionEndpointBuilder>
     finallyConvention) =>
     _finallyConventions.Add(finallyConvention);

    }

}

ActionEndpointDataSource 针对终结点的创建并不复杂:在利用IActionDescriptorCollectionProvider 对象得到所有的ActionDescriptor对象后,它将每个ActionDescriptor对象交付给CreateEndpoints来创建相应的终结点。针对约定路由的终结点列表由CreateConventionalEndpoints方法进行创建,一个ActionDescriptor对象对应”零到多个“终结点的映射规则就体现在这里。针对特性路由的ActionDescriptor对象则在CreateAttributeEndpoint方法中转换成一个单一的终结点。EndpointDataSource还通过GetChangeToken方法返回的IChangeToken 对象感知终结点的实时变化,真正的MVC框架正好利用了这一点实现了”动态模块加载“的功能。我们的模拟框架直接返回一个单例的NullChangeToken对象。

public class ActionEndpointDataSource : EndpointDataSource
{
    
    private readonly IServiceProvider _serviceProvider;
    
    private readonly IActionDescriptorCollectionProvider _actions;
    
    private readonly RoutePatternTransformer _transformer;
    
    private readonly ListActionEndpointBuilder>
    >
     _conventions = new();
    
    private readonly ListActionEndpointBuilder>
    >
     _finallyConventions = new();
    
    private int _routeOrder;
    


    private ListEndpoint>
    ? _endpoints;
    
    private readonly List(string RouteName, string Template, RouteValueDictionary? Defaults, IDictionarystring, object?>
    ? Constraints, RouteValueDictionary? DataTokens, ListActionEndpointBuilder>
    >
     Conventions, ListActionEndpointBuilder>
    >
     FinallyConventions)>
     _conventionalRoutes = new();


    public ActionEndpointDataSource(IServiceProvider serviceProvider)
    {
    
        _serviceProvider = serviceProvider;
    
        _actions = serviceProvider.GetRequiredServiceIActionDescriptorCollectionProvider>
    ();
    
        _transformer = serviceProvider.GetRequiredServiceRoutePatternTransformer>
    ();
    
        DefaultBuilder = new EndpointConventionBuilder(_conventions, _finallyConventions);

    }
    

    public override IReadOnlyListEndpoint>
     Endpoints =>
     _endpoints ??= _actions.ActionDescriptors.SelectMany(CreateEndpoints).ToList();
    
    public override IChangeToken GetChangeToken() =>
     NullChangeToken.Singleton;
    
    public IEndpointConventionBuilder AddRoute(string routeName, string pattern, RouteValueDictionary? defaults, IDictionarystring, object?>
? constraints, RouteValueDictionary? dataTokens)
    {
    
        var conventions = new ListActionEndpointBuilder>
    >
    ();
    
        var finallyConventions = new ListActionEndpointBuilder>
    >
    ();
    
        _conventionalRoutes.Add((routeName, pattern, defaults, constraints, dataTokens, conventions, finallyConventions));
    }
    
    private IEnumerableEndpoint>
 CreateEndpoints(ActionDescriptor actionDescriptor)
    {

        var routeValues = new RouteValueDictionary
        {

            {
"controller", actionDescriptor.ControllerName }
,
            {
 "action", actionDescriptor.ActionName }

        }
    ;
    
        var attributes = actionDescriptor.MethodInfo.GetCustomAttributes(true).Union(actionDescriptor.MethodInfo.DeclaringType!.GetCustomAttributes(true));
    
        var routeTemplateProvider = actionDescriptor.RouteTemplateProvider;

        if (routeTemplateProvider is null)
        {

            foreach (var endpoint in CreateConventionalEndpoints(actionDescriptor, routeValues, attributes))
            {
    
                yield return endpoint;

            }

        }

        else
        {
    
           yield return  CreateAttributeEndpoint(actionDescriptor, routeValues, attributes));

        }

    }
    
    private IEnumerableEndpoint>
     CreateConventionalEndpoints(ActionDescriptor actionDescriptor, RouteValueDictionary routeValues, IEnumerableobject>
 attributes )
    {

        foreach (var (routeName, template, defaults, constraints, dataTokens, conventionals, finallyConventionals) in _conventionalRoutes)
        {
    
            var pattern = RoutePatternFactory.Parse(template, defaults, constraints);
    
            pattern = _transformer.SubstituteRequiredValues(pattern, routeValues);

            if (pattern is not null)
            {

                var builder = new RouteEndpointBuilder(requestDelegate: HandleRequestAsync, routePattern: pattern, _routeOrder++)
                {

                    ApplicationServices = _serviceProvider
                }
    ;
    
                builder.Metadata.Add(actionDescriptor);

                foreach (var attribute in attributes)
                {
    
                    builder.Metadata.Add(attribute);

                }
    
                yield return builder.Build();

            }

        }

    }
    
    private Endpoint CreateAttributeEndpoint(ActionDescriptor actionDescriptor, RouteValueDictionary routeValues, IEnumerableobject>
 attributes)
    {
    
        var routeTemplateProvider = actionDescriptor.RouteTemplateProvider!;
    
        var pattern = RoutePatternFactory.Parse(routeTemplateProvider.Template!);

        var builder = new RouteEndpointBuilder(requestDelegate: HandleRequestAsync, routePattern: pattern, _routeOrder++)
        {

            ApplicationServices = _serviceProvider
        }
    ;
    
        builder.Metadata.Add(actionDescriptor);

        foreach (var attribute in attributes)
        {
    
            builder.Metadata.Add(attribute);

        }

        if (routeTemplateProvider is IActionHttpMethodProvider httpMethodProvider)
        {
    
            builder.Metadata.Add(new HttpMethodActionConstraint(httpMethodProvider.HttpMethods));

        }
    
        return builder.Build();

    }

}
    

三、绑定Action方法参数

现在我们完成了路由(终结点)注册,此时匹配的请求总是会被路由到对应的终结点,后者将利用IActionInvokerFactory工厂创建的IActionInvoker对象来处理请求。IActionInvoker最终需要调用对应的Action方法,但是要完成针对目标方法的调用,得先绑定其所有参数,MVC框架为此构建了一套名为“模型绑定(Model Binding)”的系统来完成参数绑定的任务,毫无疑问这是MVC框架最为复杂的部分。在我么简化的模拟框架中,我们将针对单个参数的绑定交给IArgumentBinder对象来完成。

如代码片段所示,定义在IArgumentBinder中的BindAsync方法具有两个参数,一个是当前ActionContext上下文,另一个是描述目标参数的ParameterDescriptor 对象。该方法返回类型为ValueTaskobject?> ,泛型参数代表的object就是执行Action方法得到的返回值(对于返回类型为void的方法,这个值总是Null)。默认实现的ArgumentBinder类型完成了最基本的参数绑定功能,它可以帮助我们完成源自依赖服务、请求查询字符串、路由参数、主体内容(默认采用JSON反序列化)和默认值的参数绑定。

public interface IArgumentBinder
{
    
    public ValueTaskobject?>
     BindAsync(ActionContext actionContext, ParameterDescriptor parameterDescriptor);

}


public class ArgumentBinder : IArgumentBinder
{
    
    private readonly ConcurrentDictionaryType, object?>
     _defaults = new();
    
    private readonly MethodInfo _method = typeof(ArgumentBinder).GetMethod(nameof(GetDefaultValue))!;
    
    public ValueTaskobject?>
 BindAsync(ActionContext actionContext, ParameterDescriptor parameterDescriptor)
    {
    
        var requestServices = actionContext.HttpContext.RequestServices;
    
        var parameterInfo = parameterDescriptor.ParameterInfo;
    
        var parameterName = parameterInfo.Name!;
    
        var parameterType = parameterInfo.ParameterType;
    

        // From registered service
        var result = requestServices.GetService(parameterType);

        if (result is not null)
        {
    
            return ValueTask.FromResult(result)!;

        }
    

        // From query, route, body
        var request = actionContext.HttpContext.Request;

        if (request.Query.TryGetValue(parameterName, out var value1))
        {
    
            return ValueTask.FromResult(Convert.ChangeType((string)value1!, parameterType))!;

        }

        if (request.RouteValues.TryGetValue(parameterName, out var value2))
        {
    
            return ValueTask.FromResult(Convert.ChangeType(value2, parameterType)!)!;

        }
    
        if (request.ContentLength >
 0)
        {
    
            return JsonSerializer.DeserializeAsync(request.Body, parameterType);

        }
    

        // From default value
        var defaultValue = _defaults.GetOrAdd(parameterType, type=>
     _method.MakeGenericMethod(parameterType).Invoke(null,null));
    
        return ValueTask.FromResult(defaultValue);

    }
    
    public static T GetDefaultValueT>
    () =>
     default!;

}
    

四、执行Action方法

在模拟框架中,针对目标Action方法的执行体现在如下所示的IActionMethodExecutor接口的Execute方法上,该方法的三个参数分别代表Controller对象、描述目标Action方法的ActionDescriptor和通过“参数绑定”得到的参数列表。Execute方法的返回值就是执行目标Action方法的返回值。如下所示的实现类型ActionMethodExecutor 利用“表达式树”的方式将Action方法对应的MethodInfo转换成对应的Funcobject, object?[], object?> 委托,并利用后者执行Action方法。

public interface IActionMethodExecutor
{
    
    object? Execute(object controller, ActionDescriptor actionDescriptor, object?[] arguments);

}


public class ActionMethodExecutor : IActionMethodExecutor
{
    
    private readonly ConcurrentDictionaryMethodInfo, Funcobject, object?[], object?>
    >
     _executors = new();
    
    public object? Execute(object controller, ActionDescriptor actionDescriptor, object?[] arguments)
        =>
     _executors.GetOrAdd(actionDescriptor.MethodInfo, CreateExecutor).Invoke(controller, arguments);
    
    private Funcobject, object?[], object?>
 CreateExecutor(MethodInfo methodInfo)
    {
    
        var controller = Expression.Parameter(typeof(object));
    
        var arguments = Expression.Parameter(typeof(object?[]));
    

        var parameters = methodInfo.GetParameters();
    
        var convertedArguments = new Expression[parameters.Length];
    
        for (int index = 0;
     index  parameters.Length;
 index++)
        {
    
            convertedArguments[index] = Expression.Convert(Expression.ArrayIndex(arguments, Expression.Constant(index)), parameters[index].ParameterType);

        }
    

        var convertedController = Expression.Convert(controller, methodInfo.DeclaringType!);
    
        var call = Expression.Call(convertedController, methodInfo, convertedArguments);
    
        return Expression.LambdaFuncobject, object?[], object?>
    >
    (call, controller, arguments).Compile();

    }

}

五、响应执行结果

当我们利用IActionMethodExecutor对象成功执行Action方法后,需要进一步处理其返回值。为了统一处理执行Action方法的结果,于是有了如下这个IActionResult接口,具体的处理逻辑实现在ExecuteResultAsync方法中,方法的唯一参数依然是当前ActionContext上下文。我们定义了如下这个JsonResult实现基于JSON的响应。

public interface IActionResult
{
    
    Task ExecuteResultAsync(ActionContext  actionContext);

}


public class JsonResult(object data) : IActionResult
{

    public Task ExecuteResultAsync(ActionContext actionContext)
    {
    
        var response = actionContext.HttpContext.Response;
    
        response.ContentType = "application/json";
    
        return JsonSerializer.SerializeAsync(response.Body, data);

    }

}
    

当IActionMethodExecutor成功执行目标方法后,我们会得到作为返回值的Object对象(可能是Null),如果我们能够进一步将它转换成一个IActionResult对象,一切就迎刃而解了,为此我专门定义了如下这个IActionResultConverter接口。如代码片段所示,IActionResultConverter接口的唯一方法ConvertAsync方法会将作为Action方法返回值的Object对象转化成ValueTaskIActionResult> 对象。

public interface IActionResultConverter
{
    
    ValueTaskIActionResult>
     ConvertAsync(object? result);

}


public class ActionResultConverter : IActionResultConverter
{
    
    private readonly MethodInfo _valueTaskConvertMethod = typeof(ActionResultConverter).GetMethod(nameof(ConvertFromValueTask))!;
    
    private readonly MethodInfo _taskConvertMethod = typeof(ActionResultConverter).GetMethod(nameof(ConvertFromTask))!;
    
    private readonly ConcurrentDictionaryType, Funcobject, ValueTaskIActionResult>
    >
    >
     _converters = new();
    

    public ValueTaskIActionResult>
 ConvertAsync(object? result)
    {

        // Null
        if (result is null)
        {
    
            return ValueTask.FromResultIActionResult>
    (VoidActionResult.Instance);

        }
    

        // TaskIActionResult>
    
        if (result is TaskIActionResult>
 taskOfActionResult)
        {
    
            return new ValueTaskIActionResult>
    (taskOfActionResult);

        }
    

        // ValueTaskIActionResult>
    
        if (result is ValueTaskIActionResult>
 valueTaskOfActionResult)
        {
    
            return valueTaskOfActionResult;

        }


        // IActionResult
        if (result is IActionResult actionResult)
        {
    
            return ValueTask.FromResult(actionResult);

        }


        // ValueTask
        if (result is ValueTask valueTask)
        {
    
            return Convert(valueTask);

        }
    

        // Task
        var type = result.GetType();

        if (type == typeof(Task))
        {
    
            return Convert((Task)result);

        }
    

        // ValueTaskT>
    
        if (type.IsGenericType &
    &
     type.GetGenericTypeDefinition() == typeof(ValueTask>
))
        {
    
            return _converters.GetOrAdd(type, t =>
     CreateValueTaskConverter(t, _valueTaskConvertMethod)).Invoke(result);

        }
    

        // TaskT>
    
        if (type.IsGenericType &
    &
     type.GetGenericTypeDefinition() == typeof(Task>
))
        {
    
            return _converters.GetOrAdd(type, t =>
     CreateValueTaskConverter(t, _taskConvertMethod)).Invoke(result);

        }
    

        // Object
        return ValueTask.FromResultIActionResult>
    (new ObjectActionResult(result));

    }
    

    public static async ValueTaskIActionResult>
     ConvertFromValueTaskT>
    (ValueTaskT>
 valueTask)
    {
    
        var result = valueTask.IsCompleted ? valueTask.Result : await valueTask;
    
        return result is IActionResult actionResult ? actionResult : new ObjectActionResult(result!);

    }
    

    public static async ValueTaskIActionResult>
     ConvertFromTaskT>
    (TaskT>
 task)
    {
    
        var result = await task;
    
        return result is IActionResult actionResult ? actionResult : new ObjectActionResult(result!);

    }
    

    private static async ValueTaskIActionResult>
 Convert(ValueTask valueTask)
    {
    
        if (!valueTask.IsCompleted) await valueTask;
    
        return VoidActionResult.Instance;

    }
    

    private static async ValueTaskIActionResult>
 Convert(Task task)
    {
    
        await task;
    
        return VoidActionResult.Instance;

    }
    

    private static Funcobject, ValueTaskIActionResult>
    >
 CreateValueTaskConverter(Type valueTaskType, MethodInfo convertMethod)
    {
    
        var parameter = Expression.Parameter(typeof(object));
    
        var convert = Expression.Convert(parameter, valueTaskType);
    
        var method = convertMethod.MakeGenericMethod(valueTaskType.GetGenericArguments()[0]);
    
        var call = Expression.Call(method, convert);
    
        return Expression.LambdaFuncobject, ValueTaskIActionResult>
    >
    >
    (call, parameter).Compile();

    }


    private sealed class VoidActionResult : IActionResult
    {
    
        public static readonly VoidActionResult Instance = new();
    
        public Task ExecuteResultAsync(ActionContext actionContext) =>
     Task.CompletedTask;

    }


    private sealed class ObjectActionResult(object result) : IActionResult
    {

        public Task ExecuteResultAsync(ActionContext actionContext)
        {
    
            var response = actionContext.HttpContext.Response;
    
            response.ContentType = "text/plain";
    
            return response.WriteAsync(result.ToString()!);

        }

    }

}
    

作为默认实现的ActionResultConverter 在进行转换的时候,会根据返回值的类型做针对性转换,具体的转换规则如下:

  • Null:根据单例的VoidActionResult对象创建一个ValueTaskIActionResult> ,VoidActionResult实现的ExecuteResultAsync方法什么都不要做;
  • TaskIActionResult> :直接将其转换成ValueTaskIActionResult> ;
  • ValueTaskIActionResult> :直接返回;
  • 实现了IActionResult接口:根据该对象创建ValueTaskIActionResult> ;
  • ValueTask:调用Convert方法进行转换;
  • Task:调用另一个Convert方法进行转换;
  • ValueTaskT> :调用ConvertFromValueTaskT> 方法进行转换;
  • TaskT> :调用ConvertFromTaskT> 方法进行转换;
  • 其他:根据返回创建一个ObjectActionResult对象(它会将ToString方法返回的字符串作为响应内容),并创建一个ValueTaskIActionResult> 对象。

六、编排整个处理流程

到目前为止,我们不经能够执行Action方法,还能将方法的返回值转换成ValueTaskIActionResult> 对象,定义一个完成整个请求处理的IActionInvoker实现类型就很容易了。如代码片段所示,如下这个实现了IActionInvoker接口的ActionInvoker对象是根据当前ActionContext创建的,在实现的InvokeAsync方法中,它利用ActionContext上下文提供的ActionDescriptor解析出Controller类型,并利用针对当前请求的依赖注入容器(IServiceProvider)将Controller对象创建出来。

public class ActionInvoker(ActionContext actionContext) : IActionInvoker
{

    public ActionContext ActionContext {
     get;
 }
     = actionContext;

    public async Task InvokeAsync()
    {
    

        var requestServices = ActionContext.HttpContext.RequestServices;
    

        // Create controller instance
        var controller = ActivatorUtilities.CreateInstance(requestServices, ActionContext.ActionDescriptor.MethodInfo.DeclaringType!);

        try
        {
    
            // Bind arguments
            var parameters = ActionContext.ActionDescriptor.Parameters;
    
            var arguments = new object?[parameters.Length];
    
            var binder = requestServices.GetRequiredServiceIArgumentBinder>
    ();
    
            for (int index = 0;
     index  parameters.Length;
 index++)
            {
    
                var valueTask = binder.BindAsync(ActionContext, parameters[index]);

                if (valueTask.IsCompleted)
                {
    
                    arguments[index] = valueTask.Result;

                }

                else
                {
    
                    arguments[index] = await valueTask;

                }

            }
    

            // Execute action method
            var executor = requestServices.GetRequiredServiceIActionMethodExecutor>
    ();
    

            var result = executor.Execute(controller, ActionContext.ActionDescriptor, arguments);
    

            // Convert result to IActionResult
            var converter = requestServices.GetRequiredServiceIActionResultConverter>
    ();
    
            var convert = converter.ConvertAsync(result);
    
            var actionResult = convert.IsCompleted ? convert.Result : await convert;
    

            // Execute result
            await actionResult.ExecuteResultAsync(ActionContext);

        }

        finally
        {
    
            (controller as IDisposable)?.Dispose();

        }

    }

}


public class ActionInvokerFactory : IActionInvokerFactory
{
    
    public IActionInvoker CreateInvoker(ActionContext actionContext) =>
     new ActionInvoker(actionContext);

}

接下来,它同样利用ActionDescriptor得到描述每个参数的ParameterDescriptor对象,并利用IParameterBinder完成参数绑定,最终得到一个传入Action方法的参数列表。接下来ActionInvoker利用IActionMethodExecutor对象成功执行Action方法,并利用IActionResultConverter对象将返回结果转换成IActionResult对象,最终通过执行这个对象完成针对请求的响应工作。如果Controller类型实现了IDisposable接口,在完成了整个处理流程后,我们还会调用其Dispose方法确保资源得到释放。

七、跑起来看看

当目前为止,模拟的MVC框架的核心组件均已构建完成,现在我们补充两个扩展方法。如代码片段所示,针对IServiceCollection接口的扩展方法AddControllers2(为了区别于现有的AddControllers,后面的MapControllerRoute2方法命名也是如此)将上述的接口和实现类型注册为依赖服务;针对IEndpointRouteBuilder 接口的扩展方法MapControllerRoute2完成了针对ActionEndpointDataSource的中,并在此基础上注册一个默认的约定路由。()

public static class Extensions
{

    public static IServiceCollection AddControllers2(this IServiceCollection services)
    {
    
        services.TryAddSingletonIActionInvokerFactory, ActionInvokerFactory>
    ();
    
        services.TryAddSingletonIActionMethodExecutor, ActionMethodExecutor>
    ();
    
        services.TryAddSingletonIActionResultConverter, ActionResultConverter>
    ();
    
        services.TryAddSingletonIArgumentBinder, ArgumentBinder>
    ();
    
        services.TryAddSingletonIActionDescriptorCollectionProvider, ActionDescriptorCollectionProvider>
    ();
    
        return services;

    }


    public static IEndpointConventionBuilder MapControllerRoute2(
        this IEndpointRouteBuilder endpoints,
        string name,
        [StringSyntax("Route")] string pattern,
        object? defaults = null,
        object? constraints = null,
        object? dataTokens = null)
    {
    
        var source = new ActionEndpointDataSource(endpoints.ServiceProvider);
    
        endpoints.DataSources.Add(source);
    
        return source.AddRoute(
            name,
            pattern,
            new RouteValueDictionary(defaults),
            new RouteValueDictionary(constraints),
            new RouteValueDictionary(dataTokens));

    }

}

现在我们在此基础上构建如下这个简单的MVC应用。如代码片段所示,我们调用了AddControllers扩展方法完成了核心服务的注册;调用了MapControllerRoute2扩展方法并注册了一个路径模板为“{ controller} /{ action} /{ id?} ”的约定路由。定义的HomeController类型中定义了三个Action方法。采用约定路由的Action方法Foo具有三个输入参数x、y和z,返回根据它们构建的Result对象;Action方法Bar具有相同的参数,但返回一个ValueTaskResult> 对象,我们通过标注的HttpGetAttribute特性注册了一个路径模板为“bar/{ x} /{ y} /{ z} ”的特性路由;Action方法Baz的输入参数类型为Result,返回一个ValueTaskIActionResult> 对象(具体返回的是一个JsonResult对象)。标注的HttpPostAttribute特性将路由模板设置为“/baz”。

var builder = WebApplication.CreateBuilder(args);
    
builder.Services.AddControllers2();
    
var app = builder.Build();

app.MapControllerRoute2(name: "default", pattern: "{
controller}
/{
action}
/{
id?}
    ");
    
app.Run();

public class HomeController
{
    
    public Result Foo(string x, int y, double z) =>
     new Result(x, y, z);


    [Microsoft.AspNetCore.Mvc.HttpGet("bar/{
x}
/{
y}
/{
z}
    ")]
    public ValueTaskResult>
     Bar(string x, int y, double z) =>
     ValueTask.FromResult(new Result(x, y, z));
    

    [Microsoft.AspNetCore.Mvc.HttpPost("/baz")]
    public ValueTaskIActionResult>
     Baz(Result input) =>
     ValueTask.FromResultIActionResult>
    (new JsonResult(input));

}
    

public record Result(string X, int Y, double Z);
    

应用启动后,我们通过路径“/home/foo?x=123& y=456& z=789”访问Action方法Foo,并利用查询字符串指定三个参数值。或者通过路径“/bar/123/456/789”方法ActionBar,并利用路由变量指定三个参数。我们都会得到相同的响应。

imageimage

我们使用Fiddler向路径“/baz”发送一个POST请求来访问Action方法Baz,我们将请求的主体内容设置为基于Result类型的JSON字符串,我们提供的IArgumentBinder对象利用发序列化请求主体的形式绑定其参数。由于Action方法最终会返回一个JsonResult,所以响应的内容与请求内容保持一致。

POST http://localhost:5000/baz HTTP/1.1
Host: localhost:5000
Content-Length: 29

{
"X":"123", "Y":456, "Z":789}



HTTP/1.1 200 OK
Content-Type: application/json
Date: Fri, 03 Nov 2023 06:12:15 GMT
Server: Kestrel
Content-Length: 27

{
"X":"123","Y":456,"Z":789}
    

声明:本文内容由网友自发贡献,本站不承担相应法律责任。对本内容有异议或投诉,请联系2913721942#qq.com核实处理,我们将尽快回复您,谢谢合作!


若转载请注明出处: 模拟ASP.NET Core MVC设计与实现
本文地址: https://pptw.com/jishu/572040.html
软件测试|Python对JSON的解析和创建详解 软件测试|Yarn安装指南:在不同平台上快速启动JavaScript项目

游客 回复需填写必要信息