内容概述
在本文中,我们将构建 ASP.NET Core功能的最小版本 - 是的,您没看错。我们将创建一个非常简单的 ASP.NET Core克隆来讨论整个事情是如何工作的。从一个简单的控制台应用程序开始,我们将添加必要的组件以使其作为 Web 服务器工作。我们自己的中间件管道和依赖注入的奖励积分。
ASP.NET
ASP.NET 是一个用于在 .NET 平台上构建 Web 应用程序的 Web 框架。这是一个超级丰富的功能,显然,在这篇小博客文章中,我只能涵盖其中的一小部分 - 但我还是会尝试。在这篇文章中,我将创建一个可以执行以下操作的 WebAPI :
- 它启动服务器并侦听端口(现在仅在本地主机和固定端口上)
- 我们将能够创建可以处理请求的简单控制器(目前只有 HTTP POST )
- 它将请求正文反序列化为模型并将其传递给控制器
- 控制器将返回一个模型,该模型将被序列化并发送回客户端
- 我们将利用依赖注入的力量来很好地解耦一切
- 我们将能够将中间件添加到管道中
毕竟,您希望对过于简化 ASP.NET 的工作方式有更好的理解。让我们开始吧。
控制台应用程序?
显而易见的部分是创建新的控制台应用程序。我们可以立即添加的一个小依赖项是 Microsoft.Extensions.DependencyInjection ,因为我们稍后会用到它。在我们做任何事情之前,让我们创建一个侦听端口的简单服务器:
var httpListener = new HttpListener();
httpListener.Prefixes.Add("http://localhost:5001/");
httpListener.Start();
Console.WriteLine("Listening...");
while (true)
{
// This blocks until the next request comes in
var context = httpListener.GetContext();
// ... and then we handle the request
可以这么说,这就是我们的“主”循环 - 一切都将围绕这段代码(或多或少)。 HttpListener 是 .NET 框架的一部分,用于侦听 HTTP 请求。我们为侦听器添加一个前缀,用于定义我们要侦听的端口和主机。之后,我们启动侦听器,然后进入主循环。 GetContext 方法会阻止,直到下一个请求传入。发生这种情况时,我们会得到一个 HttpListenerContext ,其中包含有关请求的所有信息。
使用方法
这是控制器外观的超级简单代码,是的,它看起来与 Asp.NetCore 中的控制器几乎相同 :
public class MyController : ControllerBase
{
[Route("api/post")]
public MyDto Call(DtoRequest request)
{
return new MyDto(request.Name);
}
[Route("api/another")]
public MyDto Another(DtoRequest request)
{
return new MyDto("Another " + request.Name);
}
ControllerBase 是我们的“标记”,以便我们的系统稍后知道什么是控制器,什么是简单的服务。为了这个讨论,它只是一个标记。
public abstract class ControllerBase { }
attribute:Route 属性用于定义控制器的路由。这只是您的日常属性:
[AttributeUsage(AttributeTargets.Method)]
public sealed class RouteAttribute : Attribute
{
public string Route { get; }
public RouteAttribute(string route)
{
Route = route;
}
}
如您所见,构造函数中只设置了一个 Route 属性,没有 HttpGet 等。从技术上讲,我们不会关心并模仿或多或少的“发布”行为。
依赖注入容器
前面我谈到了依赖注入容器,所以让我们创建一个。更重要的是,添加对用户有用的扩展方法:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddControllers(this IServiceCollection services)
{
var controllers = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => t.IsSubclassOf(typeof(ControllerBase)));
foreach (var controller in controllers)
{
services.AddSingleton(controller);
}
return services;
}
}
这个扫描我们所有的 ControllerBase 实现,并将它们作为单例添加到容器中。如果我们现在查看主要内容,我们可以做这样的事情:
var serviceProvider = new ServiceCollection()
.AddControllers()
.BuildServiceProvider();
不是那么令人印象深刻,但第一步!最后一步摆在我们面前 - 我们需要一种将路由映射到控制器的方法。因此,我们将建立一个注册表,它正是这样做的:
public class RouteRegistry
{
internal Dictionary<string, (Type Controller, MethodInfo Method)> Routes { get; } = new();
public RouteRegistry()
{
// Get all controllers
var controllers = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => t.IsSubclassOf(typeof(ControllerBase)));
foreach (var controller in controllers)
{
var methods = controller.GetMethods();
foreach (var method in methods)
{
var routeAttr = method.GetCustomAttribute<RouteAttribute>();
if (routeAttr != null)
{
// Map the route to the controller and method that will be invoked
Routes.Add(routeAttr.Route, (controller, method));
}
}
}
}
}
注册表是一个简单的类,它扫描我们所有的控制器及其方法以查找 Route 属性。如果找到一个,它会将其添加到注册表中。注册表是单一实例,将添加到 DI 容器中:
var serviceProvider = new ServiceCollection()
.AddSingleton<RouteRegistry>()
.AddControllers()
.BuildServiceProvider();
Middleware
我们缺少的最后一件事是获取该信息并使用 JSON 对象调用控制器的人,我们称之为路由中间件。这就引出了我们的奖励点。我们希望有中间件支持。基本上,请求通过管道运行。例如,我们将转到路由中间件,然后转到控制器,然后返回到路由中间件。如果我们要在路由中间件之前添加一些东西,则应首先调用该中间件,并完全控制请求,响应以及请求是否继续!
因此,让我们对所有这些进行建模。首先,我们需要一个接口:
public interface IMiddleware
{
Task InvokeAsync(HttpListenerContext context, Func<Task> next);
}
InvokeAsync 方法采用 HttpListenerContext 和表示管道中下一个中间件的 Func<Task> 。中间件可以决定是否要调用下一个中间件。如果不调用下一个中间件,则请求将不会继续。有了这个,我们还需要两件事:一个中间件管道和一个向管道添加中间件的东西。
public class MiddlewarePipeline
{
private readonly IReadOnlyList<IMiddleware> _middlewares;
public MiddlewarePipeline(IReadOnlyList<IMiddleware> middlewares)
{
_middlewares = middlewares;
}
public Task InvokeAsync(HttpListenerContext context)
{
var index = -1;
Func<Task>? nextMiddleware = null;
nextMiddleware = () =>
{
index++;
// If there are no more middlewares, return a completed task.
// Otherwise, invoke the next middleware.
return index < _middlewares.Count
? _middlewares[index].InvokeAsync(context, nextMiddleware)
: Task.CompletedTask;
};
return nextMiddleware();
}
}
以及我们注册中间件的方式:
public static IServiceCollection AddMiddleware<TMiddleware>(this IServiceCollection services)
where TMiddleware : class, IMiddleware
{
services.AddSingleton<IMiddleware, TMiddleware>();
return services;
}
完成所有基础设施后,我们可以进入最后部分:
路由中间件
路由中间件是接收请求并调用控制器的中间件。它是管道中的最后一个中间件,它不会调用下一个中间件。它将看起来像这样:
public class RoutingMiddleware
{
private readonly HttpListenerContext _context;
private readonly IServiceProvider _serviceProvider;
public RoutingMiddleware(HttpListenerContext context, private readonly IServiceProvider _serviceProvider;)
{
_context = context;
serviceProvider = _serviceProvider;
}
public async Task InvokeAsync(HttpListenerContext context, Func<Task> next)
{
if (_routeRegistry.Routes.TryGetValue(request.RawUrl![1..], out var controllerAction))
{
// Read the request body and deserialize it to the appropriate type.
using var reader = new StreamReader(context.Request.InputStream);
var requestBody = await reader.ReadToEndAsync();
// The type of object to deserialize to is determined by the method's first parameter.
var parameterType = controllerAction.Method.GetParameters()[0].ParameterType;
var requestObj = JsonSerializer.Deserialize(requestBody, parameterType);
// Fetch the controller from the DI container.
var controllerInstance = _serviceProvider.GetRequiredService(controllerAction.Controller);
// Invoke the controller method and get the result.
var actionResult = controllerAction.Method.Invoke(controllerInstance, new object[] { requestObj });
// The type of object to serialize is determined by the method's return type.
var resultJson = JsonSerializer.Serialize(actionResult);
// Write the serialized result back to the response stream.
await context.Response.OutputStream.WriteAsync(Encoding.UTF8.GetBytes(resultJson));
}
else
{
// Short-circuit the pipeline, handle not found.
context.Response.StatusCode = 404;
await context.Response.OutputStream.WriteAsync("Not Found"u8.ToArray());
}
}
}
为了更好地衡量,让我们加入另一个自定义的:
public class CustomMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpListenerContext context, Func<Task> next)
{
Console.WriteLine("Before invoking next");
await next();
Console.WriteLine("After invoking next");
}
}
有了这个,让我们看看我们的主要方法:
var serviceProvider = new ServiceCollection()
.AddSingleton<RouteRegistry>()
.AddControllers()
.AddMiddleware<CustomMiddleware>()
.AddMiddleware<RoutingMiddleware>()
.BuildServiceProvider();
// Get the list of middleware from the DI container
var middlewares = serviceProvider.GetServices<IMiddleware>().ToList();
// Create a middleware container
var middlewareContainer = new MiddlewarePipeline(middlewares);
var httpListener = new HttpListener();
httpListener.Prefixes.Add("http://localhost:5001/");
httpListener.Start();
Console.WriteLine("Listening...");
while (true)
{
var context = httpListener.GetContext();
await middlewareContainer.InvokeAsync(context);
context.Response.Close();
}
这是很多代码和工作!但我们做到了。我们有一个可以处理请求和调用控制器的工作 Web 服务器。我们还有可能添加中间件,这些中间件按注册顺序执行。让我们解雇邮递员
这里是控制台日志:
Listening...
Before invoking next
Inside RoutingMiddleware
Inside Controller
After invoking next
如您所见,请求通过中间件管道并调用控制器。控制器返回结果,结果被序列化并发送回客户端。
总的来说,多么成功!我们确实有一个正在运行的服务器,可以接受请求并以 JSON 格式返回一些内容 - 直接从我们的控制器!
我在示例中没有展示的是,您还可以将服务添加到 DI 容器并将它们注入控制器。是的,这也在工作,因为控制器是从 DI 容器中检索的。这显然也适用于您的自定义中间件。
结尾
写这篇文章很有趣,希望你读得开心。并希望您对 ASP.NET 的工作原理及其作用有更好的了解。显然,这是一个非常简单的 ASP.NET 版本,还有很多内容。
本文暂时没有评论,来添加一个吧(●'◡'●)