写点什么

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

作者:EquatorCoco
  • 2023-11-09
    福建
  • 本文字数:17452 字

    阅读完需:约 57 分钟

前几天有人在我的《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 IArgumentBinder{    public ValueTask<object?> BindAsync(ActionContext actionContext, ParameterDescriptor parameterDescriptor);}
public class ArgumentBinder : IArgumentBinder{ private readonly ConcurrentDictionary<Type, object?> _defaults = new(); private readonly MethodInfo _method = typeof(ArgumentBinder).GetMethod(nameof(GetDefaultValue))!; public ValueTask<object?> 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 GetDefaultValue<T>() => default!;}
复制代码


四、执行 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.1Host: localhost:5000Content-Length: 29
{"X":"123", "Y":456, "Z":789}

HTTP/1.1 200 OKContent-Type: application/jsonDate: Fri, 03 Nov 2023 06:12:15 GMTServer: KestrelContent-Length: 27
{"X":"123","Y":456,"Z":789}
复制代码


文章转载自:Artech

原文链接:https://www.cnblogs.com/artech/p/mvc-mini-framework.html

用户头像

EquatorCoco

关注

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
模拟ASP.NET Core MVC设计与实现_ASP.NET Core_EquatorCoco_InfoQ写作社区