编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

使用控制台模拟一个mini版的ASP.NET Core

wxchong 2024-06-24 19:41:41 开源技术 11 ℃ 0 评论

内容概述

在本文中,我们将构建 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 版本,还有很多内容。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表