前幾天有人在我的《ASP.NET Core框架揭秘》讀者群跟我留言說:“我最近在看ASP.NET Core MVC的源代碼,發(fā)現(xiàn)整個(gè)系統(tǒng)太復(fù)雜,涉及的東西太多,完全找不到方向,你能不能按照《200行代碼,7個(gè)對象——讓你了解ASP.NET Core框架的本質(zhì)》這篇文章思路剖析一下MVC框架”。對于ASP.NET Core MVC框架的涉及和實(shí)現(xiàn),說難也難,畢竟一個(gè)Model Binding就夠很多人啃很久,其實(shí)說簡單也簡單,因?yàn)檎麄€(gè)流程是很清晰的。ASP.NET Core MVC支持基于Controller和Page的兩種編程模式,雖然編程方式開起來不太一樣,底層針對請求的處理流程其實(shí)是一致的。接下來,我同樣使用簡單的代碼構(gòu)建一個(gè)Mini版的MVC框架,讓大家了解一下ASP.NET Core MVC背后的總體設(shè)計(jì),以及針對請求的處理流程。
一、描述Action方法
二、注冊路由終結(jié)點(diǎn)
三、綁定Action方法參數(shù)
四、執(zhí)行Action方法
五、響應(yīng)執(zhí)行結(jié)果
六、編排整個(gè)處理流程
七、跑起來看看
一、描述Action方法
MVC應(yīng)用提供的功能體現(xiàn)在一個(gè)個(gè)Action方法上,所以MVC框架定義了專門的類型ActionDescriptor來描述每個(gè)有效的Action方法。但是Action方法和ActionDescriptor對象并非一對一的關(guān)系,而是一對多的關(guān)系。具體來說,采用“約定路由”的Action方法對應(yīng)一個(gè)ActionDescriptor對象,如果采用“特性路由”,MVC框架會針對每個(gè)注冊的路由創(chuàng)建一個(gè)ActionDescriptor。Action方法與ActionDescriptor之間的映射關(guān)系可以通過如下這個(gè)演示實(shí)例來驗(yàn)證。如代碼片段所示,我們調(diào)用MapControllerRoute擴(kuò)展方法注冊了4個(gè)“約定路由”。HomeController類中定義了兩個(gè)合法的Action方法,其中方法Foo采用“約定路由”,而方法Bar通過標(biāo)注的兩個(gè)HttpGetAttribute特性注冊了兩個(gè)“特性路由”。按照上述的規(guī)則,將有三個(gè)ActionDescriptor被創(chuàng)建出來,方法Foo有一個(gè),而方法Bar有兩個(gè)。
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.OfType<ControllerActionDescriptor>()) { 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)}"; }
我們注冊了一個(gè)指向路徑“/actions”的路由終結(jié)點(diǎn)將所有ActionDescriptor列出來。如代碼片段所示,路由處理委托(Lambda表達(dá)式)注入了IActionDescriptorCollectionProvider 對象,我們利用它的ActionDescriptors屬性得到當(dāng)前應(yīng)用承載的所有ActionDescriptor對象。我們將其轉(zhuǎn)化成ControllerActionDescriptor(派生于ActionDescriptor,用于描述定義在Controller類型中的Action方法,另一個(gè)派生類PageActionDescriptor用于描述定義在Page類型的Action方法),并將對應(yīng)的Controller類型和方法名稱,以及特性路由模板輸出來。如下所示的輸出結(jié)果驗(yàn)證了上述針對Action方法與ActionDescriptor映射關(guān)系的論述。
在模擬框架中,我們ActionDescriptor類型作最大的簡化。如代碼片段所示,創(chuàng)建一個(gè)ActionDescriptor對象時(shí)只需提供描述目標(biāo)Action方法的MethodInfo對象(必需),和一個(gè)用來定義特性路由的IRouteTemplateProvider對象(可選,僅針對特性路由)。我們利用MethodInfo的聲明類型得到Controller的類型,將剔除“Controller”后綴的類型名稱作為ControllerName屬性(表示Controller的名稱),作為Action名稱的ActionName屬性則直接返回方法名稱。Parameters屬性返回一個(gè)ParameterDescriptor數(shù)組,而根據(jù)ParameterInfo對象構(gòu)建的ParameterDescriptor是對參數(shù)的描述。
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; }
當(dāng)前應(yīng)用涉及的所有ActionActionDescriptor由IActionDescriptorCollectionProvider對象的ActionDescriptors屬性來提供。實(shí)現(xiàn)類型ActionDescriptorCollectionProvider 從當(dāng)前啟動程序集中提取有效的Controller類型,并將定義其中的有效Action方法轉(zhuǎn)換成ActionDescriptor對象。用于定義“特性路由”的IRouteTemplateProvider對象來源于標(biāo)注到方法上的特性(簡單起見,我們忽略了標(biāo)注到Controller類型上的特性),比如HttpGetAttribute特性等,同一個(gè)Action方法針對注冊的特性路由來創(chuàng)建ActionDescriptor就體現(xiàn)在這里。
public interface IActionDescriptorCollectionProvider { IReadOnlyList<ActionDescriptor> ActionDescriptors { get; } } public class ActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider { private readonly Assembly _assembly; private List<ActionDescriptor>? _actionDescriptors; public IReadOnlyList<ActionDescriptor> ActionDescriptors => _actionDescriptors ??= Resolve(_assembly.GetExportedTypes()).ToList(); public ActionDescriptorCollectionProvider(IWebHostEnvironment environment) { var assemblyName = new AssemblyName(environment.ApplicationName); _assembly = Assembly.Load(assemblyName); } private IEnumerable<ActionDescriptor> Resolve(IEnumerable<Type> 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().OfType<IRouteTemplateProvider>(); 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; }
二、注冊路由終結(jié)點(diǎn)
MVC利用“路由”對外提供服務(wù),它會將每個(gè)ActionDescriptor轉(zhuǎn)換成“零到多個(gè)”路由終結(jié)點(diǎn)。ActionDescriptor與終結(jié)點(diǎn)之間的對應(yīng)關(guān)系為什么是“零到多”,而不是“一對一”或者“一對多”呢?這也與Action方法采用的路由默認(rèn)有關(guān),采用特性路由的ActionDescriptor(RouteTemplateProvider 屬性不等于Null)總是對應(yīng)著一個(gè)確定的路由,但是如何為采用“約定路由”的ActionDescriptor創(chuàng)建對應(yīng)的終結(jié)點(diǎn),則取決于多少個(gè)約定路由與之匹配。針對每一個(gè)基于“約定”路由的ActionDescriptor,系統(tǒng)會為每個(gè)與之匹配的路由創(chuàng)建對應(yīng)的終結(jié)點(diǎn)。如果沒有匹配的約定路由,對應(yīng)的Action方法自然就不會有對應(yīng)的終結(jié)點(diǎn)。
我還是利用上面演示實(shí)例來說明ActionDescriptor與路由終結(jié)點(diǎn)之間的映射關(guān)系。為此我們注冊如下這個(gè)指向路徑“/endpoints”的路由終結(jié)點(diǎn),我們通過注入的EndpointDataSource 對象得到終結(jié)點(diǎn)列表。由于針對某個(gè)Action方法創(chuàng)建的路由終結(jié)點(diǎn)都會將ActionDescriptor對象作為元數(shù)據(jù),所以我們試著將它(具體類型為ControllerActionDescriptor)提取出來,并輸出Controller類型和Action方法的名稱,以及路由模板。
... app.MapGet("/endpoints", (EndpointDataSource source) => { var builder = new StringBuilder(); foreach (var endpoint in source.Endpoints.OfType<RouteEndpoint>()) { var action = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>(); if (action is not null) { builder.AppendLine($"{action.ControllerTypeInfo.Name}.{action.MethodInfo.Name}({endpoint.RoutePattern.RawText})"); } } return builder.ToString(); }); ...
從如下所示的輸出結(jié)果可以看出,由于Action方法Bar采用“特性路由”,所以對應(yīng)的ActionDescriptor分別對應(yīng)著一個(gè)終結(jié)點(diǎn)。采用約定路由的Foo方法雖然只有一個(gè)ActionDescriptor,但是注冊的4個(gè)約定路由有兩個(gè)與它匹配(兩個(gè)必要的路由參數(shù)“controller”和“action”需要定義在路由模板中),所以它也具有兩個(gè)終結(jié)點(diǎn)。
接下來我們在模擬框架中以最簡單的方式完成“路由注冊”。我們知道每個(gè)路由終結(jié)點(diǎn)由“路由模式”和“路由處理器”這兩個(gè)核心元素構(gòu)成,前者對應(yīng)一個(gè)RoutePattern對象,由注冊的路由信息構(gòu)建而成,后者體現(xiàn)為一個(gè)用來處理請求的RequestDelegate委托。一個(gè)MVC應(yīng)用絕大部分的請求處理工作都落在IActionInvoker對象上,所以作為路由處理器的RequestDelegate委托只需要將請求處理任務(wù)“移交”給這個(gè)對象就可以了。如代碼片段所示,IActionInvoker接口定義了一個(gè)無參、返回類型為Task的InvokeAsync方法。IActionInvoker不是一個(gè)單例對象,而是針對每個(gè)請求單獨(dú)創(chuàng)建的,創(chuàng)建它的工廠由IActionInvokerFactory接口表示。如代碼片段所示,定義在該接口的工廠方法CreateInvoker利用指定的ActionContext上下文來創(chuàng)建返回的IActionInvoker對象。ActionContext可以視為MVC應(yīng)用的請求上下文,我們的模擬框架同樣對它做了最大的簡化,將它定義對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; }
我們將路由(終結(jié)點(diǎn))注冊實(shí)現(xiàn)在一個(gè)派生自EndpointDataSource的ActionEndpointDataSource類型中 。對于注冊的每個(gè)終結(jié)點(diǎn),作為處理器的RequestDelegate委托指向HandleAsync方法,可以看出這個(gè)方法的定義非常簡單:它從當(dāng)前終結(jié)點(diǎn)中以元數(shù)據(jù)的形式將ActionDescriptor對象,然后利用它與當(dāng)前HttpContext將ActionContext上下文創(chuàng)建出來。我們將此ActionContext上下文傳遞給IActionInvokerFactory工廠將IActionInvoker對象創(chuàng)建出來,并利用它完成后續(xù)的請求處理。
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.GetMetadata<ActionDescriptor>() ?? throw new InvalidOperationException("No ActionDescriptor is attached to the endpoint as metadata."); var actionContext = new ActionContext(httpContext, actionDescriptor); return httpContext.RequestServices.GetRequiredService<IActionInvokerFactory>().CreateInvoker(actionContext).InvokeAsync(); } }
ActionEndpointDataSource 定義了一個(gè)AddRoute方法來定義約定路由,注冊的約定路由被存儲在字段_conventionalRoutes所示的列表中。該方法返回一個(gè)EndpointConventionBuilder 對象,后者實(shí)現(xiàn)了IEndpointConventionBuilder 接口,我們可以利用它對添加的約定約定路由作進(jìn)一步設(shè)置(比如添加元數(shù)據(jù))。
public class ActionEndpointDataSource : EndpointDataSource { private readonly List<(string RouteName, string Template, RouteValueDictionary? Defaults, IDictionary<string, object?>? Constraints, RouteValueDictionary? DataTokens, List<Action<EndpointBuilder>> Conventions, List<Action<EndpointBuilder>> FinallyConventions)> _conventionalRoutes = new(); public IEndpointConventionBuilder AddRoute(string routeName, string pattern, RouteValueDictionary? defaults, IDictionary<string, object?>? constraints, RouteValueDictionary? dataTokens) { var conventions = new List<Action<EndpointBuilder>>(); var finallyConventions = new List<Action<EndpointBuilder>>(); _conventionalRoutes.Add((routeName, pattern, defaults, constraints, dataTokens, conventions, finallyConventions)); return new EndpointConventionBuilder(conventions, finallyConventions); }
private sealed class EndpointConventionBuilder : IEndpointConventionBuilder { private readonly List<Action<EndpointBuilder>> _conventions; private readonly List<Action<EndpointBuilder>> _finallyConventions; public EndpointConventionBuilder(List<Action<EndpointBuilder>> conventions, List<Action<EndpointBuilder>> finallyConventions) { _conventions = conventions; _finallyConventions = finallyConventions; } public void Add(Action<EndpointBuilder> convention) => _conventions.Add(convention); public void Finally(Action<EndpointBuilder> finallyConvention) => _finallyConventions.Add(finallyConvention); } }
ActionEndpointDataSource 針對終結(jié)點(diǎn)的創(chuàng)建并不復(fù)雜:在利用IActionDescriptorCollectionProvider 對象得到所有的ActionDescriptor對象后,它將每個(gè)ActionDescriptor對象交付給CreateEndpoints來創(chuàng)建相應(yīng)的終結(jié)點(diǎn)。針對約定路由的終結(jié)點(diǎn)列表由CreateConventionalEndpoints方法進(jìn)行創(chuàng)建,一個(gè)ActionDescriptor對象對應(yīng)”零到多個(gè)“終結(jié)點(diǎn)的映射規(guī)則就體現(xiàn)在這里。針對特性路由的ActionDescriptor對象則在CreateAttributeEndpoint方法中轉(zhuǎn)換成一個(gè)單一的終結(jié)點(diǎn)。EndpointDataSource還通過GetChangeToken方法返回的IChangeToken 對象感知終結(jié)點(diǎn)的實(shí)時(shí)變化,真正的MVC框架正好利用了這一點(diǎn)實(shí)現(xiàn)了”動態(tài)模塊加載“的功能。我們的模擬框架直接返回一個(gè)單例的NullChangeToken對象。
public class ActionEndpointDataSource : EndpointDataSource { private readonly IServiceProvider _serviceProvider; private readonly IActionDescriptorCollectionProvider _actions; private readonly RoutePatternTransformer _transformer; private readonly List<Action<EndpointBuilder>> _conventions = new(); private readonly List<Action<EndpointBuilder>> _finallyConventions = new(); private int _routeOrder; private List<Endpoint>? _endpoints; private readonly List<(string RouteName, string Template, RouteValueDictionary? Defaults, IDictionary<string, object?>? Constraints, RouteValueDictionary? DataTokens, List<Action<EndpointBuilder>> Conventions, List<Action<EndpointBuilder>> FinallyConventions)> _conventionalRoutes = new(); public ActionEndpointDataSource(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; _actions = serviceProvider.GetRequiredService<IActionDescriptorCollectionProvider>(); _transformer = serviceProvider.GetRequiredService<RoutePatternTransformer>(); DefaultBuilder = new EndpointConventionBuilder(_conventions, _finallyConventions); } public override IReadOnlyList<Endpoint> Endpoints => _endpoints ??= _actions.ActionDescriptors.SelectMany(CreateEndpoints).ToList(); public override IChangeToken GetChangeToken() => NullChangeToken.Singleton; public IEndpointConventionBuilder AddRoute(string routeName, string pattern, RouteValueDictionary? defaults, IDictionary<string, object?>? constraints, RouteValueDictionary? dataTokens) { var conventions = new List<Action<EndpointBuilder>>(); var finallyConventions = new List<Action<EndpointBuilder>>(); _conventionalRoutes.Add((routeName, pattern, defaults, constraints, dataTokens, conventions, finallyConventions));
}
private IEnumerable<Endpoint> 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 IEnumerable<Endpoint> CreateConventionalEndpoints(ActionDescriptor actionDescriptor, RouteValueDictionary routeValues, IEnumerable<object> 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, IEnumerable<object> 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方法參數(shù)
現(xiàn)在我們完成了路由(終結(jié)點(diǎn))注冊,此時(shí)匹配的請求總是會被路由到對應(yīng)的終結(jié)點(diǎn),后者將利用IActionInvokerFactory工廠創(chuàng)建的IActionInvoker對象來處理請求。IActionInvoker最終需要調(diào)用對應(yīng)的Action方法,但是要完成針對目標(biāo)方法的調(diào)用,得先綁定其所有參數(shù),MVC框架為此構(gòu)建了一套名為“模型綁定(Model Binding)”的系統(tǒng)來完成參數(shù)綁定的任務(wù),毫無疑問這是MVC框架最為復(fù)雜的部分。在我么簡化的模擬框架中,我們將針對單個(gè)參數(shù)的綁定交給IArgumentBinder對象來完成。
如代碼片段所示,定義在IArgumentBinder中的BindAsync方法具有兩個(gè)參數(shù),一個(gè)是當(dāng)前ActionContext上下文,另一個(gè)是描述目標(biāo)參數(shù)的ParameterDescriptor 對象。該方法返回類型為ValueTask<object?>,泛型參數(shù)代表的object就是執(zhí)行Action方法得到的返回值(對于返回類型為void的方法,這個(gè)值總是Null)。默認(rèn)實(shí)現(xiàn)的ArgumentBinder類型完成了最基本的參數(shù)綁定功能,它可以幫助我們完成源自依賴服務(wù)、請求查詢字符串、路由參數(shù)、主體內(nèi)容(默認(rèn)采用JSON反序列化)和默認(rèn)值的參數(shù)綁定。
public interface IActionMethodExecutor { object? Execute(object controller, ActionDescriptor actionDescriptor, object?[] arguments); } public class ActionMethodExecutor : IActionMethodExecutor { private readonly ConcurrentDictionary<MethodInfo, Func<object, object?[], object?>> _executors = new(); public object? Execute(object controller, ActionDescriptor actionDescriptor, object?[] arguments) => _executors.GetOrAdd(actionDescriptor.MethodInfo, CreateExecutor).Invoke(controller, arguments); private Func<object, 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); var convertResult = Expression.Convert(call, typeof(object)); return Expression.Lambda<Func<object, object?[], object?>>(convertResult, controller, arguments).Compile(); } }
四、執(zhí)行Action方法
在模擬框架中,針對目標(biāo)Action方法的執(zhí)行體現(xiàn)在如下所示的IActionMethodExecutor接口的Execute方法上,該方法的三個(gè)參數(shù)分別代表Controller對象、描述目標(biāo)Action方法的ActionDescriptor和通過“參數(shù)綁定”得到的參數(shù)列表。Execute方法的返回值就是執(zhí)行目標(biāo)Action方法的返回值。如下所示的實(shí)現(xiàn)類型ActionMethodExecutor 利用“表達(dá)式樹”的方式將Action方法對應(yīng)的MethodInfo轉(zhuǎn)換成對應(yīng)的Func<object, object?[], object?>委托,并利用后者執(zhí)行Action方法。
public interface IActionMethodExecutor { object? Execute(object controller, ActionDescriptor actionDescriptor, object?[] arguments); } public class ActionMethodExecutor : IActionMethodExecutor { private readonly ConcurrentDictionary<MethodInfo, Func<object, object?[], object?>> _executors = new(); public object? Execute(object controller, ActionDescriptor actionDescriptor, object?[] arguments) => _executors.GetOrAdd(actionDescriptor.MethodInfo, CreateExecutor).Invoke(controller, arguments); private Func<object, 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.Lambda<Func<object, object?[], object?>>(call, controller, arguments).Compile(); } }
五、響應(yīng)執(zhí)行結(jié)果
當(dāng)我們利用IActionMethodExecutor對象成功執(zhí)行Action方法后,需要進(jìn)一步處理其返回值。為了統(tǒng)一處理執(zhí)行Action方法的結(jié)果,于是有了如下這個(gè)IActionResult接口,具體的處理邏輯實(shí)現(xiàn)在ExecuteResultAsync方法中,方法的唯一參數(shù)依然是當(dāng)前ActionContext上下文。我們定義了如下這個(gè)JsonResult實(shí)現(xiàn)基于JSON的響應(yīng)。
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); } }
當(dāng)IActionMethodExecutor成功執(zhí)行目標(biāo)方法后,我們會得到作為返回值的Object對象(可能是Null),如果我們能夠進(jìn)一步將它轉(zhuǎn)換成一個(gè)IActionResult對象,一切就迎刃而解了,為此我專門定義了如下這個(gè)IActionResultConverter接口。如代碼片段所示,IActionResultConverter接口的唯一方法ConvertAsync方法會將作為Action方法返回值的Object對象轉(zhuǎn)化成ValueTask<IActionResult>對象。
public interface IActionResultConverter { ValueTask<IActionResult> 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 ConcurrentDictionary<Type, Func<object, ValueTask<IActionResult>>> _converters = new(); public ValueTask<IActionResult> ConvertAsync(object? result) { // Null if (result is null) { return ValueTask.FromResult<IActionResult>(VoidActionResult.Instance); } // Task<IActionResult> if (result is Task<IActionResult> taskOfActionResult) { return new ValueTask<IActionResult>(taskOfActionResult); } // ValueTask<IActionResult> if (result is ValueTask<IActionResult> 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); } // ValueTask<T> if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ValueTask<>)) { return _converters.GetOrAdd(type, t => CreateValueTaskConverter(t, _valueTaskConvertMethod)).Invoke(result); } // Task<T> if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>)) { return _converters.GetOrAdd(type, t => CreateValueTaskConverter(t, _taskConvertMethod)).Invoke(result); } // Object return ValueTask.FromResult<IActionResult>(new ObjectActionResult(result)); } public static async ValueTask<IActionResult> ConvertFromValueTask<T>(ValueTask<T> valueTask) { var result = valueTask.IsCompleted ? valueTask.Result : await valueTask; return result is IActionResult actionResult ? actionResult : new ObjectActionResult(result!); } public static async ValueTask<IActionResult> ConvertFromTask<T>(Task<T> task) { var result = await task; return result is IActionResult actionResult ? actionResult : new ObjectActionResult(result!); } private static async ValueTask<IActionResult> Convert(ValueTask valueTask) { if (!valueTask.IsCompleted) await valueTask; return VoidActionResult.Instance; } private static async ValueTask<IActionResult> Convert(Task task) { await task; return VoidActionResult.Instance; } private static Func<object, ValueTask<IActionResult>> 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.Lambda<Func<object, ValueTask<IActionResult>>>(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()!); } } }
作為默認(rèn)實(shí)現(xiàn)的ActionResultConverter 在進(jìn)行轉(zhuǎn)換的時(shí)候,會根據(jù)返回值的類型做針對性轉(zhuǎn)換,具體的轉(zhuǎn)換規(guī)則如下:
- Null:根據(jù)單例的VoidActionResult對象創(chuàng)建一個(gè)ValueTask<IActionResult>,VoidActionResult實(shí)現(xiàn)的ExecuteResultAsync方法什么都不要做;
- Task<IActionResult>:直接將其轉(zhuǎn)換成ValueTask<IActionResult>;
- ValueTask<IActionResult>:直接返回;
- 實(shí)現(xiàn)了IActionResult接口:根據(jù)該對象創(chuàng)建ValueTask<IActionResult>;
- ValueTask:調(diào)用Convert方法進(jìn)行轉(zhuǎn)換;
- Task:調(diào)用另一個(gè)Convert方法進(jìn)行轉(zhuǎn)換;
- ValueTask<T>:調(diào)用ConvertFromValueTask<T>方法進(jìn)行轉(zhuǎn)換;
- Task<T>:調(diào)用ConvertFromTask<T>方法進(jìn)行轉(zhuǎn)換;
- 其他:根據(jù)返回創(chuàng)建一個(gè)ObjectActionResult對象(它會將ToString方法返回的字符串作為響應(yīng)內(nèi)容),并創(chuàng)建一個(gè)ValueTask<IActionResult>對象。
六、編排整個(gè)處理流程
到目前為止,我們不經(jīng)能夠執(zhí)行Action方法,還能將方法的返回值轉(zhuǎn)換成ValueTask<IActionResult>對象,定義一個(gè)完成整個(gè)請求處理的IActionInvoker實(shí)現(xiàn)類型就很容易了。如代碼片段所示,如下這個(gè)實(shí)現(xiàn)了IActionInvoker接口的ActionInvoker對象是根據(jù)當(dāng)前ActionContext創(chuàng)建的,在實(shí)現(xiàn)的InvokeAsync方法中,它利用ActionContext上下文提供的ActionDescriptor解析出Controller類型,并利用針對當(dāng)前請求的依賴注入容器(IServiceProvider)將Controller對象創(chuàng)建出來。
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.GetRequiredService<IArgumentBinder>(); 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.GetRequiredService<IActionMethodExecutor>(); var result = executor.Execute(controller, ActionContext.ActionDescriptor, arguments); // Convert result to IActionResult var converter = requestServices.GetRequiredService<IActionResultConverter>(); 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得到描述每個(gè)參數(shù)的ParameterDescriptor對象,并利用IParameterBinder完成參數(shù)綁定,最終得到一個(gè)傳入Action方法的參數(shù)列表。接下來ActionInvoker利用IActionMethodExecutor對象成功執(zhí)行Action方法,并利用IActionResultConverter對象將返回結(jié)果轉(zhuǎn)換成IActionResult對象,最終通過執(zhí)行這個(gè)對象完成針對請求的響應(yīng)工作。如果Controller類型實(shí)現(xiàn)了IDisposable接口,在完成了整個(gè)處理流程后,我們還會調(diào)用其Dispose方法確保資源得到釋放。
七、跑起來看看
當(dāng)目前為止,模擬的MVC框架的核心組件均已構(gòu)建完成,現(xiàn)在我們補(bǔ)充兩個(gè)擴(kuò)展方法。如代碼片段所示,針對IServiceCollection接口的擴(kuò)展方法AddControllers2(為了區(qū)別于現(xiàn)有的AddControllers,后面的MapControllerRoute2方法命名也是如此)將上述的接口和實(shí)現(xiàn)類型注冊為依賴服務(wù);針對IEndpointRouteBuilder 接口的擴(kuò)展方法MapControllerRoute2完成了針對ActionEndpointDataSource的中,并在此基礎(chǔ)上注冊一個(gè)默認(rèn)的約定路由。()
public static class Extensions { public static IServiceCollection AddControllers2(this IServiceCollection services) { services.TryAddSingleton<IActionInvokerFactory, ActionInvokerFactory>(); services.TryAddSingleton<IActionMethodExecutor, ActionMethodExecutor>(); services.TryAddSingleton<IActionResultConverter, ActionResultConverter>(); services.TryAddSingleton<IArgumentBinder, ArgumentBinder>(); services.TryAddSingleton<IActionDescriptorCollectionProvider, 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)); } }
現(xiàn)在我們在此基礎(chǔ)上構(gòu)建如下這個(gè)簡單的MVC應(yīng)用。如代碼片段所示,我們調(diào)用了AddControllers擴(kuò)展方法完成了核心服務(wù)的注冊;調(diào)用了MapControllerRoute2擴(kuò)展方法并注冊了一個(gè)路徑模板為“{controller}/{action}/{id?}”的約定路由。定義的HomeController類型中定義了三個(gè)Action方法。采用約定路由的Action方法Foo具有三個(gè)輸入?yún)?shù)x、y和z,返回根據(jù)它們構(gòu)建的Result對象;Action方法Bar具有相同的參數(shù),但返回一個(gè)ValueTask<Result>對象,我們通過標(biāo)注的HttpGetAttribute特性注冊了一個(gè)路徑模板為“bar/{x}/{y}/{z}”的特性路由;Action方法Baz的輸入?yún)?shù)類型為Result,返回一個(gè)ValueTask<IActionResult>對象(具體返回的是一個(gè)JsonResult對象)。標(biāo)注的HttpPostAttribute特性將路由模板設(shè)置為“/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 ValueTask<Result> Bar(string x, int y, double z) => ValueTask.FromResult(new Result(x, y, z)); [Microsoft.AspNetCore.Mvc.HttpPost("/baz")] public ValueTask<IActionResult> Baz(Result input) => ValueTask.FromResult<IActionResult>(new JsonResult(input)); } public record Result(string X, int Y, double Z);
應(yīng)用啟動后,我們通過路徑“/home/foo?x=123&y=456&z=789”訪問Action方法Foo,并利用查詢字符串指定三個(gè)參數(shù)值。或者通過路徑“/bar/123/456/789”方法ActionBar,并利用路由變量指定三個(gè)參數(shù)。我們都會得到相同的響應(yīng)。
我們使用Fiddler向路徑“/baz”發(fā)送一個(gè)POST請求來訪問Action方法Baz,我們將請求的主體內(nèi)容設(shè)置為基于Result類型的JSON字符串,我們提供的IArgumentBinder對象利用發(fā)序列化請求主體的形式綁定其參數(shù)。由于Action方法最終會返回一個(gè)JsonResult,所以響應(yīng)的內(nèi)容與請求內(nèi)容保持一致。
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}