模擬ASP.NET Core MVC設計與實現

2023-11-08 09:00:29

前幾天有人在我的《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.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)}";
}

我們註冊了一個指向路徑「/actions」的路由終結點將所有ActionDescriptor列出來。如程式碼片段所示,路由處理委託(Lambda表示式)注入了IActionDescriptorCollectionProvider 物件,我們利用它的ActionDescriptors屬性得到當前應用承載的所有ActionDescriptor物件。我們將其轉化成ControllerActionDescriptor(派生於ActionDescriptor,用於描述定義在Controller型別中的Action方法,另一個派生類PageActionDescriptor用於描述定義在Page型別的Action方法),並將對應的Controller型別和方法名稱,以及特性路由模板輸出來。如下所示的輸出結果驗證了上述針對Action方法與ActionDescriptor對映關係的論述。

image

在模擬框架中,我們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
{
    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;
}

二、註冊路由終結點

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.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();
});
...

從如下所示的輸出結果可以看出,由於Action方法Bar採用「特性路由」,所以對應的ActionDescriptor分別對應著一個終結點。採用約定路由的Foo方法雖然只有一個ActionDescriptor,但是註冊的4個約定路由有兩個與它匹配(兩個必要的路由引數「controller」和「action」需要定義在路由模板中),所以它也具有兩個終結點。

image

接下來我們在模擬框架中以最簡單的方式完成「路由註冊」。我們知道每個路由終結點由「路由模式」和「路由處理器」這兩個核心元素構成,前者對應一個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.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 定義了一個AddRoute方法來定義約定路由,註冊的約定路由被儲存在欄位_conventionalRoutes所示的列表中。該方法返回一個EndpointConventionBuilder 物件,後者實現了IEndpointConventionBuilder 介面,我們可以利用它對新增的約定約定路由作進一步設定(比如新增後設資料)。

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 針對終結點的建立並不複雜:在利用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 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方法引數

現在我們完成了路由(終結點)註冊,此時匹配的請求總是會被路由到對應的終結點,後者將利用IActionInvokerFactory工廠建立的IActionInvoker物件來處理請求。IActionInvoker最終需要呼叫對應的Action方法,但是要完成針對目標方法的呼叫,得先繫結其所有引數,MVC框架為此構建了一套名為「模型繫結(Model Binding)」的系統來完成引數繫結的任務,毫無疑問這是MVC框架最為複雜的部分。在我麼簡化的模擬框架中,我們將針對單個引數的繫結交給IArgumentBinder物件來完成。

如程式碼片段所示,定義在IArgumentBinder中的BindAsync方法具有兩個引數,一個是當前ActionContext上下文,另一個是描述目標引數的ParameterDescriptor 物件。該方法返回型別為ValueTask<object?>,泛型引數代表的object就是執行Action方法得到的返回值(對於返回型別為void的方法,這個值總是Null)。預設實現的ArgumentBinder型別完成了最基本的引數繫結功能,它可以幫助我們完成源自依賴服務、請求查詢字串、路由引數、主體內容(預設採用JSON反序列化)和預設值的引數繫結。

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();
    }
}

四、執行Action方法

在模擬框架中,針對目標Action方法的執行體現在如下所示的IActionMethodExecutor介面的Execute方法上,該方法的三個引數分別代表Controller物件、描述目標Action方法的ActionDescriptor和通過「引數繫結」得到的參數列。Execute方法的返回值就是執行目標Action方法的返回值。如下所示的實現型別ActionMethodExecutor 利用「表示式樹」的方式將Action方法對應的MethodInfo轉換成對應的Func<object, object?[], object?>委託,並利用後者執行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();
    }
}

五、響應執行結果

當我們利用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物件轉化成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()!);
        }
    }
}

作為預設實現的ActionResultConverter 在進行轉換的時候,會根據返回值的型別做針對性轉換,具體的轉換規則如下:

  • Null:根據單例的VoidActionResult物件建立一個ValueTask<IActionResult>,VoidActionResult實現的ExecuteResultAsync方法什麼都不要做;
  • Task<IActionResult>:直接將其轉換成ValueTask<IActionResult>;
  • ValueTask<IActionResult>:直接返回;
  • 實現了IActionResult介面:根據該物件建立ValueTask<IActionResult>;
  • ValueTask:呼叫Convert方法進行轉換;
  • Task:呼叫另一個Convert方法進行轉換;
  • ValueTask<T>:呼叫ConvertFromValueTask<T>方法進行轉換;
  • Task<T>:呼叫ConvertFromTask<T>方法進行轉換;
  • 其他:根據返回建立一個ObjectActionResult物件(它會將ToString方法返回的字串作為響應內容),並建立一個ValueTask<IActionResult>物件。

六、編排整個處理流程

到目前為止,我們不經能夠執行Action方法,還能將方法的返回值轉換成ValueTask<IActionResult>物件,定義一個完成整個請求處理的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.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得到描述每個引數的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.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));
    }
}

現在我們在此基礎上構建如下這個簡單的MVC應用。如程式碼片段所示,我們呼叫了AddControllers擴充套件方法完成了核心服務的註冊;呼叫了MapControllerRoute2擴充套件方法並註冊了一個路徑模板為「{controller}/{action}/{id?}」的約定路由。定義的HomeController型別中定義了三個Action方法。採用約定路由的Action方法Foo具有三個輸入引數x、y和z,返回根據它們構建的Result物件;Action方法Bar具有相同的引數,但返回一個ValueTask<Result>物件,我們通過標註的HttpGetAttribute特性註冊了一個路徑模板為「bar/{x}/{y}/{z}」的特性路由;Action方法Baz的輸入引數型別為Result,返回一個ValueTask<IActionResult>物件(具體返回的是一個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 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);

應用啟動後,我們通過路徑「/home/foo?x=123&y=456&z=789」存取Action方法Foo,並利用查詢字串指定三個引數值。或者通過路徑「/bar/123/456/789」方法ActionBar,並利用路由變數指定三個引數。我們都會得到相同的響應。

image

我們使用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}