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

前几天有人在我的《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 servicevar result = requestServices.GetService(parameterType);if (result is not null){return ValueTask.FromResult(result)!;}// From query, route, bodyvar 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 valuevar 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){// Nullif (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;}// IActionResultif (result is IActionResult actionResult){return ValueTask.FromResult(actionResult);}// ValueTaskif (result is ValueTask valueTask){return Convert(valueTask);}// Taskvar 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);}// Objectreturn 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 instancevar controller = ActivatorUtilities.CreateInstance(requestServices, ActionContext.ActionDescriptor.MethodInfo.DeclaringType!);try{// Bind argumentsvar 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 methodvar executor = requestServices.GetRequiredService<IActionMethodExecutor>();var result = executor.Execute(controller, ActionContext.ActionDescriptor, arguments);// Convert result to IActionResultvar converter = requestServices.GetRequiredService<IActionResultConverter>();var convert = converter.ConvertAsync(result);var actionResult = convert.IsCompleted ? convert.Result : await convert;// Execute resultawait 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}

文章转载自:Artech

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/190157.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

css实现进度条

预期样式 方法一 <script setup> import { ref } from "vue"; // import ScreenLeft from "./ScreenLeft/index.vue"; const width ref("76.5%"); </script><template>Screen<div class"progress-contain">…

详解数据仓库之拉链表(原理、设计以及在Hive中的实现)

最近发现一本好书&#xff0c;读完感觉讲的非常好&#xff0c;首先安利给大家&#xff0c;国内第一本系统讲解数据血缘的书&#xff01;点赞&#xff01;近几天也会安排朋友圈点赞赠书活动(ง•̀_•́)ง 0x00 前言 本文将会谈一谈在数据仓库中拉链表相关的内容&#xff0c;包…

ZYNQ_project:key_beep

通过按键控制蜂鸣器工作。 模块框图&#xff1a; 时序图&#xff1a; 代码&#xff1a; /*1位按键消抖 */ module key_filter (input wire sys_clk ,input wire sys_rst_n ,input wire key_in ,output …

springboot项目使用Swagger3

一、Swagger介绍 号称世界上最流行的Api框架&#xff1b;Restful Api 文档在线自动生成工具>Api文档与API定义同步更新直接运行&#xff0c;可以在在线测试API 接口支持多种语言&#xff1a;&#xff08;java&#xff0c;Php…&#xff09; 二、Swagger3 准备工作 1、在p…

VsCode 安装 GitHub Copilot插件 (最新)

##在线安装&#xff1a; 打开Vscode扩展商店&#xff0c;输入 "GitHub Copilot " ,选择下载人数最多的那个。&#xff08;这个是你写一部分代码或者注释&#xff0c;Ai自动帮你提示/补全代码&#xff09;,建议选择这个 注意下面有个和他类似的 "GitHub Copilo…

BMVC 23丨多模态CLIP:用于3D场景问答任务的对比视觉语言预训练

来源&#xff1a;投稿 作者&#xff1a;橡皮 编辑&#xff1a;学姐 论文链接&#xff1a;https://arxiv.org/abs/2306.02329 摘要&#xff1a; 训练模型将常识性语言知识和视觉概念从 2D 图像应用到 3D 场景理解是研究人员最近才开始探索的一个有前景的方向。然而&#xff0c…

APS、SAP解析BOM批量核对(我的APS项目三)

APS提供了解析BOM接口 SAP从CU50中解析了BOM 博主开发了一个程序&#xff0c;把两边的BOM数据拉到一起来比对&#xff0c;从最初的一个车型&#xff0c;增加到5个车型&#xff0c;最后成型是30个车型&#xff0c;几乎覆盖了F1、F2的全部车型。 并且程序还实现了消息提醒功能&…

Kotlin(十) 空指针检查、字符串内嵌表达式以及函数默认值

空指针检查 我们在之前的章节里&#xff0c;有定义一个Study的类&#xff0c;它有两个函数&#xff0c;一个doHomework(),一个readBooks()。然后我们定义个doStudy函数&#xff0c;来调用它们&#xff0c;代码如下&#xff1a; fun doStudy(study: Study) {study.doHomework(…

直播间自动发言机器人的运行分享,与开发需要到的技术分析

先来看实操成果&#xff0c;↑↑需要的同学可看我名字↖↖↖↖↖&#xff0c;或评论888无偿分享 一、引言 随着人工智能技术的不断发展&#xff0c;自动发言机器人已经成为了当今社交媒体领域的重要组成部分。它们能够自动化地发布内容、回复用户评论和消息&#xff0c;大大提高…

RE切入点:选择SLI,设定SLO

还是先来复习下上节课讲的“系统可用性”的两种计算方式&#xff0c;一种是从故障角度出发&#xff0c;以时长维度对系统进行稳定性评估&#xff1b;另一种是从成功请求占比角度出发&#xff0c;以请求维度对系统进行稳定性评估。同时&#xff0c;我们还讲到&#xff0c;在 SRE…

Django中简单的增删改查

用户列表展示 建立列表 views.py def userlist(request):return render(request,userlist.html) urls.py urlpatterns [path(admin/, admin.site.urls),path(userlist/, views.userlist), ]templates----userlist.html <!DOCTYPE html> <html lang"en">…

【开源项目】snakeflow流程引擎研究

项目地址 https://gitee.com/yuqs/snakerflow https://toscode.mulanos.cn/zc-libre/snakerflow-spring-boot-stater &#xff08;推荐&#xff09; https://github.com/snakerflow-starter/snakerflow-spring-boot-starter 常用API 部署流程 processId engine.process().de…

Adversarial Training Methods for Deep Learning: A Systematic Review

Adversarial Training Methods for Deep Learning: A Systematic Review----《面向深度学习的对抗训练方法:系统回顾》 摘要 通过快速梯度符号法(FGSM)、投影梯度下降法(PGD)和其他攻击算法&#xff0c;深度神经网络暴露在对抗攻击的风险下。对抗性训练是用来防御对抗性攻击威…

CoRL 2023 获奖论文公布,manipulation、强化学习等主题成热门

今年大模型及具身智能领域有了非常多的突破性进展&#xff0c;作为机器人学与机器学习交叉领域的全球顶级学术会议之一&#xff0c;CoRL也得到了更多的关注。 CoRL 是面向机器人学习的顶会&#xff0c;涵盖机器人学、机器学习和控制等多个主题&#xff0c;包括理论与应用。今年…

USB拦截工具

USB 闪存驱动器对组织的安全和数据构成了独特的威胁。它们的便携性和充足的存储容量使它们成为数据盗窃的便捷媒介。 什么是 USB 拦截器 USB&#xff08;通用串行总线&#xff09;阻止程序用于禁用插入可移动存储设备的端口&#xff0c;便携性和充足的存储容量使 USB 成为可能…

一文了解芯片测试项目和检测方法 -纳米软件

芯片检测是芯片设计、生产、制造成过程中的关键环节&#xff0c;检测芯片的质量、性能、功能等&#xff0c;以满足设计要求和市场需求&#xff0c;确保芯片可以长期稳定运行。芯片测试内容众多&#xff0c;检测方法多样&#xff0c;今天纳米软件将为您介绍芯片的检测项目都有哪…

电脑小Tip---外接键盘F1-F12快捷键与笔记本不同步

当笔记本外接一款非常好用的静音键盘后&#xff0c;会出现一些问题。例如&#xff1a;外接键盘F1-F12与笔记本不同步。具体一个例子就是&#xff0c;在运行matlab程序时&#xff0c;需要点编辑器—运行&#xff0c;这样就很麻烦&#xff0c;直接运行的快捷键是笔记本键盘上的F5…

macOS文本编辑器 BBEdit 最新 for mac

BBEdit是一款功能强大的文本编辑器&#xff0c;适用于Mac操作系统。它由Bare Bones Software开发&#xff0c;旨在为开发者和写作人员提供专业级的文本编辑工具。 以下是BBEdit的一些主要特点和功能&#xff1a; 多语言支持&#xff1a;BBEdit支持多种编程语言和标记语言&…

[WSL] 安装hive3.1.2成功后, 使用datagrip连接失败

org.apache.hadoop.ipc.RemoteException:User: xxx is not allowed to impersonate anonymous 下载driver-hive-jdbc-3.1.2-standalone 解决 修改hadoop 配置文件 etc/hadoop/core-site.xml,加入如下配置项 <property><name>hadoop.proxyuser.你的用户名.hosts…