换个角度学习ASP.NET Core中间件

本文涉及的产品
性能测试 PTS,5000VUM额度
Serverless 应用引擎免费试用套餐包,4320000 CU,有效期3个月
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介:

换个角度学习ASP.NET Core中间件

中间件真面目
关于ASP.NET Core中间件是啥,简单一句话描述就是:用来处理HTTP请求和响应的一段逻辑,并且可以决定是否把请求传递到管道中的下一个中间件!

上面只是概念上的一种文字描述,那问题来了,中间件在程序中到底是个啥❓

一切还是从IApplicationBuilder说起,没错,就是大家熟悉的Startup类里面那个Configure方法里面的那个IApplicationBuilder(有点绕😵,抓住重点就行)。

IApplicationBuilder,应用构建者,听这个名字就能感受它的核心地位,ASP.NET Core应用就是依赖它构建出来,看看它的定义:

public interface IApplicationBuilder
{

//...省略部分代码...
IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
RequestDelegate Build();

}
Use方法用来把中间件添加到应用管道中,此时我们已经看到中间件的真面目了,原来是一个委托,输入参数是RequestDelegate,返回也是RequestDelegate,其实RequestDelegate还是个委托,如下:

public delegate Task RequestDelegate(HttpContext context);
还记得中间件是干嘛的吗?是用来处理http请求和响应的,即对HttpContext的处理,这里我们可以看出来原来中间件的业务逻辑就是封装在RequestDelegate里面。

总结一下:

middleware就是Func,输入的是下一个中间件的业务处理逻辑,返回的就是当前中间件的业务处理逻辑,并在其中决定要不要调用下个中间件!我们代码实现一个中间件看看(可能和我们平时用的不太一样,但它就是中间件最原始的形式!):

//Startup.Configure方法中
Func middleware1 = next => async (context) =>

       {
           //处理http请求

           Console.WriteLine("do something before invoke next middleware in middleware1");
           //调用下一个中间件逻辑,当然根据业务实际情况,也可以不调用,那此时中间件管道调用到此就结束来了!

           await next.Invoke(context);
           Console.WriteLine("do something after invoke next middleware in middleware1");
       };

//添加到应用中

app.Use(middleware1);

跑一下瞅瞅,成功执行中间件!

IIS Express is running.
info: Microsoft.Hosting.Lifetime[0]

  Application started. Press Ctrl+C to shut down.

info: Microsoft.Hosting.Lifetime[0]

  Hosting environment: Development

info: Microsoft.Hosting.Lifetime[0]

  Content root path: E:\vs2019Project\WebApplication3\WebApplication3

do something before invoke next middleware in middleware1
do something after invoke next middleware in middleware1
中间件管道
通过上面我们有没有注意到,添加中间时,他们都是一个一个独立的被添加进去,而中间件管道就是负责把中间件串联起来,实现下面的一个中间件调用流转流程:

如何实现呢?这个就是IApplicationBuilder中的Build的职责了,再次看下定义:

public interface IApplicationBuilder
{
//...省略部分代码...
IApplicationBuilder Use(Func middleware);
RequestDelegate Build();
}
Build方法一顿操作猛如虎,主要干一件事把中间件串联起来,最后返回了一个 RequestDelegate,而这个就是我们添加的第一个中间件返回的RequestDelegate,

看下框架默认实现:

//ApplicationBuilder.cs
public RequestDelegate Build()

    {
        RequestDelegate app = context =>
        {
            // If we reach the end of the pipeline, but we have an endpoint, then something unexpected has happened.
            // This could happen if user code sets an endpoint, but they forgot to add the UseEndpoint middleware.
            var endpoint = context.GetEndpoint();
            var endpointRequestDelegate = endpoint?.RequestDelegate;
            if (endpointRequestDelegate != null)
            {
                var message =
                    $"The request reached the end of the pipeline without executing the endpoint: '{endpoint.DisplayName}'. " +
                    $"Please register the EndpointMiddleware using '{nameof(IApplicationBuilder)}.UseEndpoints(...)' if using " +
                    $"routing.";
                throw new InvalidOperationException(message);
            }

            context.Response.StatusCode = 404;
            return Task.CompletedTask;
        };

        foreach (var component in _components.Reverse())
        {
            app = component(app);
        }

        return app;
    }

Build方法里面定义了一个 RequestDelegate ,作为最后一个处理逻辑,例如返回404。
_components存储着添加的所有中间件
中间件管道调度顺序,就是按照中间添加的顺序调用,所以中间件的顺序很重要,很重要,很重要!
遍历_components,传入next RequestDelegate,获取当前RequestDelegate,完成管道构建!
中间件使用
在此之前,还是提醒下,中间件最原始的使用姿势就是

IApplicationBuilder Use(Func middleware);
下面使用的方式,都是对此方式的扩展!

Lamda方式
大多数教程里面都提到的方式,直接上代码:

//扩展方法
//IApplicationBuilder Use(this IApplicationBuilder app, Func, Task> middleware)
app.Use(async (context, next) =>

       {
           Console.WriteLine("in m1");
           await next.Invoke();
           Console.WriteLine("out m1");
       });

扩展方法简化了中间件的使用,这个里面就只负责写核心逻辑,然后扩展方法中把它包装成Func类型进行添加,不像原始写的那样复杂,我们看下这个扩展方法实现,哈,原来就是一个简单封装!我们只要专注在middleware里面写核心业务逻辑即可。

public static IApplicationBuilder Use(this IApplicationBuilder app, Func, Task> middleware)

    {
        return app.Use(next =>
        {
            return context =>
            {
                Func<Task> simpleNext = () => next(context);
                return middleware(context, simpleNext);
            };
        });
    }

如果我们定义中间件作为终端中间件(管道流转此中间件就结束了,不再调用后面的中间件)使用时,上面只要不调用next即可。

当然我们还有另外一个选择,自己使用扩展Run方法,传入的参数就是RequestDelegate,还是上代码:

//扩展方法
//public static void Run(this IApplicationBuilder app, RequestDelegate handler);
app.Run(async (context) =>

        {
            Console.WriteLine("in m3");
            await context.Response.WriteAsync("test22");
            Console.WriteLine("out m3");
        });

到此,我们有没有发现上面的方式有些弊端,只能处理下简单逻辑,如果要依赖第三方服务,那可怎么办?

定义中间件类方式
使用中间件类,我们只要按照约定的方式,即类中包含InvokeAsync方法,就可以了。

使用类后,我们就可以注入我们需要的第三方服务,然后完成更复杂的业务逻辑,上代码

//定义第三方服务
public interface ITestService

{
    Task Test(HttpContext context);
}
public class TestService : ITestService
{
    private int _times = 0;
    public Task Test(HttpContext context)
    {
       return context.Response.WriteAsync($"{nameof(TestService)}.{nameof(TestService.Test)} is called {++_times} times\n");
    }
}

//添加到IOC容器
public void ConfigureServices(IServiceCollection services)

    {
        services.AddTransient<ITestService, TestService>();
    }

//中间件类,注入ITestService
public class CustomeMiddleware1

{
    private int _cnt;
    private RequestDelegate _next;
    private ITestService _testService;
    public CustomeMiddleware1(RequestDelegate next, ITestService testService)
    {
        _next = next;
        _cnt = 0;
        _testService = testService;
    }
    public async Task InvokeAsync(HttpContext context)
    {
        await _testService?.Test(context);
        await context.Response.WriteAsync($"{nameof(CustomeMiddleware1)} invoked {++_cnt} times");

    }
}

//添加中间件,还是一个扩展方法,预知详情,请看源码
app.UseMiddleware();
运行一下,跑出来的结果如下,完美!

等一下,有没有发现上面有啥问题???❓

明明ITestService是以Transient注册到容器里面,应该每次使用都是新实例化的,那不应该被显示被调用 15 次啊!!!

这个时候我们应该发现,我们上面的所有方式添加的中间件的生命周期其实和应用程序是一致的,也就是说是只在程序启动的时候实例化一次!所以这里第三方的服务,然后以Transient方式注册到容器,但在中间件里面变现出来就是一个单例效果,这就为什么我们不建议在中间件里面注入DbContext了,因为DbContext我们一般是以Scoped来用的,一次http请求结束,我们就要释放它!

如果我们就是要在中间件里面是有ITestService,而且还是Transient的效果,怎么办?

实现IMiddleware接口
//接口定义
public interface IMiddleware
{
    ask InvokeAsync(HttpContext context, RequestDelegate next);
}
//实现接口
public class CustomeMiddleware : IMiddleware

{
    private int _cnt;
    private ITestService _testService;
    public CustomeMiddleware(ITestService testService)
    {
        _cnt = 0;
        _testService = testService;
    }
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        await _testService?.Test(context);
        await context.Response.WriteAsync($"{nameof(CustomeMiddleware)} invoked {++_cnt} times");

    }
}

//添加中间件
app.UseMiddleware();
运行一下,结果报错了... ,提示CustomeMiddleware没有注册!

InvalidOperationException: No service for type 'WebApplication3.CustomeMiddleware' has been registered.
通过报错信息,我们已经知道,如果实现了IMiddleware接口的中间件,他们并不是在应用启动时就实例化好的,而是每次都是从IOC容器中获取的,其中就是IMiddlewareFactory

来解析出对应类型的中间件的(内部就是调用IServiceProvider),了解到此,我们就知道,此类中间件此时是需要以service的方式注册到IOC容器里面的,这样中间件就可以根据注册时候指定的生命周期方式来实例化,从而解决了我们上一节提出的疑问了!好了,我们注册下中间件服务

public void ConfigureServices(IServiceCollection services)

    {
        services.AddTransient<CustomeMiddleware>();
        services.AddTransient<ITestService, TestService>();
    }

再次多次刷新请求,返回都是下面的内容

TestService.Test is called 1 times
CustomeMiddleware invoked 1 times
结语
中间件存在这么多的使用方式,每一个存在都是为了解决实际需求的,当我们了解这些背景知识后,在后面自己使用时,就能更加的灵活!

作者:小伟06

出处:https://www.cnblogs.com/liuww/p/12944937.html

相关文章
|
1月前
|
存储 开发框架 JSON
ASP.NET Core OData 9 正式发布
【10月更文挑战第8天】Microsoft 在 2024 年 8 月 30 日宣布推出 ASP.NET Core OData 9,此版本与 .NET 8 的 OData 库保持一致,改进了数据编码以符合 OData 规范,并放弃了对旧版 .NET Framework 的支持,仅支持 .NET 8 及更高版本。新版本引入了更快的 JSON 编写器 `System.Text.UTF8JsonWriter`,优化了内存使用和序列化速度。
|
1月前
mcr.microsoft.com/dotnet/core/aspnet:2.1安装libgdiplus
mcr.microsoft.com/dotnet/core/aspnet:2.1安装libgdiplus
29 1
|
2月前
|
开发框架 监控 前端开发
在 ASP.NET Core Web API 中使用操作筛选器统一处理通用操作
【9月更文挑战第27天】操作筛选器是ASP.NET Core MVC和Web API中的一种过滤器,可在操作方法执行前后运行代码,适用于日志记录、性能监控和验证等场景。通过实现`IActionFilter`接口的`OnActionExecuting`和`OnActionExecuted`方法,可以统一处理日志、验证及异常。创建并注册自定义筛选器类,能提升代码的可维护性和复用性。
|
2月前
|
开发框架 .NET 中间件
ASP.NET Core Web 开发浅谈
本文介绍ASP.NET Core,一个轻量级、开源的跨平台框架,专为构建高性能Web应用设计。通过简单步骤,你将学会创建首个Web应用。文章还深入探讨了路由配置、依赖注入及安全性配置等常见问题,并提供了实用示例代码以助于理解与避免错误,帮助开发者更好地掌握ASP.NET Core的核心概念。
96 3
|
1月前
|
开发框架 JavaScript 前端开发
一个适用于 ASP.NET Core 的轻量级插件框架
一个适用于 ASP.NET Core 的轻量级插件框架
|
6月前
|
消息中间件 存储 负载均衡
消息中间件的选择:RabbitMQ是一个明智的选择
消息中间件的选择:RabbitMQ是一个明智的选择
111 0
|
5月前
|
消息中间件 存储 中间件
【消息中间件】详解三大MQ:RabbitMQ、RocketMQ、Kafka
【消息中间件】详解三大MQ:RabbitMQ、RocketMQ、Kafka
1404 0
|
4月前
|
消息中间件 编解码 Docker
Docker部署RabbitMQ消息中间件
【7月更文挑战第4天】Docker部署RabbitMQ消息中间件
277 3
|
1月前
|
消息中间件 编解码 Docker
【Docker项目实战】Docker部署RabbitMQ消息中间件
【10月更文挑战第8天】Docker部署RabbitMQ消息中间件
89 1
【Docker项目实战】Docker部署RabbitMQ消息中间件
|
3月前
|
消息中间件 Java 测试技术
消息中间件RabbitMQ---SpringBoot整合RabbitMQ【三】
这篇文章是关于如何在SpringBoot应用中整合RabbitMQ的消息中间件。内容包括了在SpringBoot项目中添加RabbitMQ的依赖、配置文件设置、启动类注解,以及如何通过单元测试来创建交换器、队列、绑定,并发送和接收消息。文章还介绍了如何配置消息转换器以支持对象的序列化和反序列化,以及如何使用注解`@RabbitListener`来接收消息。
消息中间件RabbitMQ---SpringBoot整合RabbitMQ【三】