《200行代碼,7個對象——讓你了解ASP.NET Core框架的本質》讓很多讀者對ASP.NET Core管道有了真實的了解。在過去很長一段時間中,有很多人私信給我:能否按照相同的方式分析一下MVC框架的設計與實現原理,希望這篇文章能夠滿足你們的需求。我們在《[上篇]:路由整合》將定義在Controller類型中的Action方法簡化成只返回Task或者Void的方法,並讓方法自身去完成包括對請求予以相應的所有請求處理任務,但真實的MVC框架並非如此。真正的MVC框架中具有一個名為IActionResult的重要結構,顧名思義,IActionResult對象一般會作為Action方法的返回值,針對請求的響應任務基本上會由這個對象來實現。
源代碼下載:
IActionResult的執行
IActionResult的類型轉換
一、IActionResult
作為Action方法執行結果旨在對請求做最終響應的IActionResult接口同樣具有極為簡單的定義。如下main的代碼片段所示,IActionResult對象針對請求的響應實現在它唯一的ExecuteResultAsync方法中,針對待執行Action的ActionContext上下文是其唯一的輸入參數。
public interface IActionResult { Task ExecuteResultAsync(ActionContext context); }
針對不同的請求響應需求,MVC框架為我們定義了一系列的IActionResult實現類型,應用程序同樣也可以根據需要定義自己的IActionResult類型。作為演示,我們定義了如下這個ContentResult類型,它將指定的字符串作為響應主體的內容,具體的內容類型(媒體內容或者MIME類型)則可以靈活指定。
public class ContentResult : IActionResult { private readonly string _content; private readonly string _contentType; public ContentResult(string content, string contentType) { _content = content; _contentType = contentType; } public Task ExecuteResultAsync(ActionContext context) { var response = context.HttpContext.Response; response.ContentType = _contentType; return response.WriteAsync(_content); } }
由於Action方法可能沒有返回值,為了使Action執行流程(執行Action方法=>將返回值轉化成IActionResult對象=>執行IActionResult對象)顯得明確而清晰,我們定義了如下這個“什么都沒做”的NullActionResult類型,它利用靜態只讀屬性Instance返回一個單例的NullActionResult對象。
public sealed class NullActionResult : IActionResult { private NullActionResult() { } public static NullActionResult Instance { get; } = new NullActionResult(); public Task ExecuteResultAsync(ActionContext context) => Task.CompletedTask; }
二、執行IActionResult對象
接下來我們將Action方法返回類型的約束放寬,除了Task和Void,Action方法的返回類型還可以是IActionResult、Task<IActionResult>和ValueTask<IActionResult>。基於這個新的約定,我們需要對前面定義的ControllerActionInvoker的InvokeAsync方法作如下的修改。如代碼片段所示,在執行目標Action方法之后,我們調用ToActionResultAsync方法將返回對象轉換成一個Task<IActionResult>對象,最終針對請求的響應只需要直接執行這個IActionResult對象即可。
public class ControllerActionInvoker : IActionInvoker { public ActionContext ActionContext { get; } public ControllerActionInvoker(ActionContext actionContext) => ActionContext = actionContext; 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 result = actionMethod.Invoke(controllerInstance, new object[0]); var actionResult = await ToActionResultAsync(result); await actionResult.ExecuteResultAsync(ActionContext); } private async Task<IActionResult> ToActionResultAsync(object result) { if (result == null) { return NullActionResult.Instance; } if (result is Task<IActionResult> taskOfActionResult) { return await taskOfActionResult; } if (result is ValueTask<IActionResult> valueTaskOfActionResult) { return await valueTaskOfActionResult; } if (result is IActionResult actionResult) { return actionResult; } if (result is Task task) { await task; return NullActionResult.Instance; } throw new InvalidOperationException("Action method's return value is invalid."); } }
我們接下來將前面定義的ContentResult引入到演示實例的FoobarController中。如下面的代碼片段所示,我們將Action方法FooAsync和Bar的返回類型分別替換成Task<IActionResult>和IActionResult,具體返回的都是一個ContentResult對象。兩個ContentResult對象都將同一段HTML片段作為響應的主體內容,但是FooAsync方法將內容類型設置成 “text/html” ,而Bar方法則將其設置為 “text/plain” 。
public class FoobarController : Controller { private static readonly string _html = @"<html> <head> <title>Hello</title> </head> <body> <p>Hello World!</p> </body> </html>"; [HttpGet("/{foo}")] public Task<IActionResult> FooAsync() { return Task.FromResult<IActionResult>(new ContentResult(_html, "text/html")); } public IActionResult Bar() => new ContentResult(_html, "text/plain"); }
演示程序啟動之后,如果采用與前面一樣的URL訪問定義在FoobarController的兩個Action方法,我們會在瀏覽器上得到如下圖所示的輸出結果。由於FooAsync方法將內容類型設置為 “text/html” ,所以瀏覽器會將返回的內容作為一個HTML文檔進行解析,但是Bar方法將內容類型設置為 “text/plain” ,所以返回的內容會原封不動地輸出到瀏覽器上。源代碼從這里下載。
三、IActionResult類型轉化
前面的內容對Task方法的返回類型做出了一系列的約束,但是我們知道在真正的MVC框架中,定義在Controller中的Action方法可以采用任意的類型。為了解決這個問題,我們可以考慮Action方法返回的數據對象轉換成一個IActionResult對象。我們將類型轉換規則定義成通過IActionResultTypeMapper接口表示的服務,針對IActionResult的類型轉換體現在Convert方法上。值得一提的是,Convert方法表示待轉換的對象的value參數並不一定是Action方法的返回值,而是具體數據對象。如果Action方法的返回值是一個Task<TResult>或者ValueTask<TResult>對象,它們的Result屬性返回的參數這個待轉換的數據對象。
public interface IActionResultTypeMapper { IActionResult Convert(object value, Type returnType); }
簡單起見,我們定義了如下這個ActionResultTypeMapper類型將作為模擬框架對IActionResultTypeMapper接口的默認實現。如代碼片段所示,Convert方法將返回個內容類型為“text/plain”的ContentResult對象,原始對象字符串描述(ToString方法的返回值)將作為響應主題的內容。
public class ActionResultTypeMapper : IActionResultTypeMapper { public IActionResult Convert(object value, Type returnType) => new ContentResult(value.ToString(), "text/plain"); }
當我們將針對Action方法返回類型的限制去除之后,我們的ControllerActionInvoker自然需要作進一步修改。Action方法可能會返回一個Task<TResult>或者ValueTask<TResult>對象(泛型參數TResult可以是任意類型),所以我們在ControllerActionInvoker類型定義了如下兩個靜態方法(ConvertFromTaskAsync<TValue>和ConvertFromValueTaskAsync<TValue>)將它們轉換成Task<IActionResult>對象,如果返回的不是一個IActionResult對象,作為參數的IActionResultTypeMapper對象將來進行類型轉換。我們定義在兩個靜態只讀字段(_taskConvertMethod和_valueTaskConvertMethod)來保存描述這兩個泛型方法的MethodInfo對象。
public class ControllerActionInvoker : IActionInvoker { private static readonly MethodInfo _taskConvertMethod; private static readonly MethodInfo _valueTaskConvertMethod; static ControllerActionInvoker() { var bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static; _taskConvertMethod = typeof(ControllerActionInvoker).GetMethod(nameof(ConvertFromTaskAsync), bindingFlags); _valueTaskConvertMethod = typeof(ControllerActionInvoker).GetMethod(nameof(ConvertFromValueTaskAsync), bindingFlags); } private static async Task<IActionResult> ConvertFromTaskAsync<TValue>(Task<TValue> returnValue, IActionResultTypeMapper mapper) { var result = await returnValue; return result is IActionResult actionResult ? actionResult : mapper.Convert(result, typeof(TValue)); } private static async Task<IActionResult> ConvertFromValueTaskAsync<TValue>( ValueTask<TValue> returnValue, IActionResultTypeMapper mapper) { var result = await returnValue; return result is IActionResult actionResult ? actionResult : mapper.Convert(result, typeof(TValue)); } … }
如下所示的是InvokeAsync方法針對Action的執行。在執行了目標Action方法並得到原始的返回值后,我們調用了ToActionResultAsync方法將返回值轉換成Task<IActionResult>,最終通過執行IActionResult對象進而完成所有的請求處理任務。如果返回類型為Task<TResult>或者ValueTask<TResult>,我們會直接采用反射的方式調用ConvertFromTaskAsync<TValue>或者ConvertFromValueTaskAsync<TValue>方法(更好的方式是采用表達式樹的方式執行類型轉換方法以獲得更好的性能)。
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 returnValue = actionMethod.Invoke(controllerInstance, new object[0]); var mapper = requestServies.GetRequiredService<IActionResultTypeMapper>(); var actionResult = await ToActionResultAsync( returnValue, actionMethod.ReturnType, mapper); await actionResult.ExecuteResultAsync(ActionContext); } private Task<IActionResult> ToActionResultAsync(object returnValue, Type returnType, IActionResultTypeMapper mapper) { //Null if (returnValue == null || returnType == typeof(Task) || returnType == typeof(ValueTask)) { return Task.FromResult<IActionResult>(NullActionResult.Instance); } //IActionResult if (returnValue is IActionResult actionResult) { return Task.FromResult(actionResult); } //Task<TResult> if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>)) { var declaredType = returnType.GenericTypeArguments.Single(); var taskOfResult = _taskConvertMethod.MakeGenericMethod(declaredType).Invoke(null, new object[] { returnValue, mapper }); return (Task<IActionResult>)taskOfResult; } //ValueTask<TResult> if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) { var declaredType = returnType.GenericTypeArguments.Single(); var valueTaskOfResult = _valueTaskConvertMethod.MakeGenericMethod(declaredType).Invoke(null, new object[] { returnValue, mapper }); return (Task<IActionResult>)valueTaskOfResult; } return Task.FromResult(mapper.Convert(returnValue, returnType)); } }
從上面的代碼片段可以看出,在進行針對IActionResult的類型轉換過程中使用到的IActionResultTypeMapper對象是從針對當前請求的依賴注入容器中提取的,所以我們在應用啟動之前需要作針對性的服務注冊。我們將針對IActionResultTypeMapper的服務注冊添加到之前定義的AddMvcControllers擴展方法中。
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>(); } }
為了驗證模擬框架對Action方法的任意返回類型的支持,我們將前面演示實例定義的FoobarController做了如下的修改。如代碼片段所示,我們在FoobarController類型中定義了四個Action方法,它們返回的類型分別為Task<ContentResult>、ValueTask<ContentResult>、Task<String>、ValueTask<String>,ContentResult對象的內容和直接返回的字符串都是一段相同的HTML。
public class FoobarController : Controller { private static readonly string _html = @"<html> <head> <title>Hello</title> </head> <body> <p>Hello World!</p> </body> </html>"; [HttpGet("/foo")] public Task<ContentResult> FooAsync() => Task.FromResult(new ContentResult(_html, "text/html")); [HttpGet("/bar")] public ValueTask<ContentResult> BarAsync() => new ValueTask<ContentResult>(new ContentResult(_html, "text/html")); [HttpGet("/baz")] public Task<string> BazAsync() => Task.FromResult(_html); [HttpGet("/qux")] public ValueTask<string> QuxAsync() => new ValueTask<string>(_html); }
我們在上述四個Action方法上通過標注HttpGetAttribute特性將路由模板分別設置為“/foo”、“/bar”、“/baz”和“/qux”,所以我們可以采用相應的URL來訪問這四個Action方法。下圖所示的是這個Action的響應內容在瀏覽器上的呈現。由於Action方法Baz和Qux返回的是一個字符串,按照ActionResultTypeMapper類型提供的轉換規則,最終返回的將是以此字符串作為響應內容,內容類型為 “text/plain” 的ContentResult對象。源代碼從這里下載。
通過極簡模擬框架讓你了解ASP.NET Core MVC框架的設計與實現[上篇]:路由整合
通過極簡模擬框架讓你了解ASP.NET Core MVC框架的設計與實現[中篇]: 請求響應
通過極簡模擬框架讓你了解ASP.NET Core MVC框架的設計與實現[下篇]:參數綁定


