.NET Core 开发实战
  • 目录
  • 第1课:课程介绍
  • 第2课:内容综述
  • 第3课:.NET Core的现状、未来以及环境搭建
  • 第4课:Startup:掌握ASP.NET Core的启动过程
  • 第5课:依赖注入:良好架构的起点
  • 第6课:作用域与对象释放行为
  • 第7课:用Autofac增强容器能力
  • 第8课:配置框架:让服务无缝适应各种环境
  • 第9课:命令行配置提供程序
  • 第10课:环境变量配置提供程序
  • 第11课:文件配置提供程序
  • 第12课:配置变更监听
  • 第13课:配置绑定:使用强类型对象承载配置数据
  • 第14课:自定义配置数据源:低成本实现定制化配置方案
  • 第15课:选项框架:服务组件集成配置的最佳实践
  • 第16课:选项数据热更新:让服务感知配置的变化
  • 第17课:为选项数据添加验证:避免错误配置的应用接收用户流量
  • 第18课:日志框架:聊聊记日志的最佳姿势
  • 第19课:日志作用域:解决不同请求之间的日志干扰
  • 第20课:结构化日志组件Serilog:记录对查询分析友好的日志
  • 第21课:中间件:掌控请求处理过程的关键
  • 第22课:异常处理中间件:区分真异常与逻辑异常
  • 第23课:静态文件中间件:前后端分离开发合并部署骚操作
  • 第24课:文件提供程序:让你可以将文件放在任何地方
  • 第25课:路由与终结点:如何规划好你的Web API
  • 第26课:工程结构概览:定义应用分层及依赖关系
  • 第27课:定义Entity:区分领域模型的内在逻辑和外在行为
  • 第28课:工作单元模式(UnitOfWork):管理好你的事务
  • 第29课:定义仓储:使用EF Core实现仓储层
  • 第30课:领域事件:提升业务内聚,实现模块解耦
  • 第31课:APIController:定义API的最佳实践
  • 第32课:集成事件:解决跨微服务的最终一致性
  • 第33课:集成事件:使用RabbitMQ来实现EventBus
  • 第34课:MediatR:轻松实现命令查询职责分离模式(CQRS)
  • 第35课:MediatR:让领域事件处理更加优雅
Powered by GitBook
On this page

第21课:中间件:掌控请求处理过程的关键

学习分享 丨作者 / 郑 子 铭 丨公众号 / DotNet NB / CloudNative NB

Previous第20课:结构化日志组件Serilog:记录对查询分析友好的日志Next第22课:异常处理中间件:区分真异常与逻辑异常

Last updated 3 years ago

这一节讲解一下如何通过中间件来管理请求处理过程

中间件工作原理

next 表示后面有一个委托,每一层每一层套下去可以在任意的中间件来决定在后面的中间件之前执行什么,或者说在所有中间件执行完之后执行什么

整个中间件的处理过程实际上有两个核心对象:

IApplicationBuilder

RequestDelegate:处理整个请求的委托

IApplicationBuilder

namespace Microsoft.AspNetCore.Builder
{
  public interface IApplicationBuilder
  {
    IServiceProvider ApplicationServices { get; set; }

    IDictionary<string, object> Properties { get; }

    IFeatureCollection ServerFeatures { get; }

    // 最终它会 Build 返回一个委托
    // 这个委托就是把所有的中间件串起来之后,合并成一个委托方法
    // 这个方法的入参可以看下方委托的定义
    RequestDelegate Build();

    IApplicationBuilder New();

    // 它可以让我们去注册我们的中间件,把委托注册进去,每一个委托的入参也是一个委托
    // 这也就意味着可以把这些委托注册成一个链,就像上面的图显示的那样
    IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
  }
}

委托的定义

namespace Microsoft.AspNetCore.Http
{
  // 委托的入参是 HttpContext,所有的注册中间件的委托实际上都是对 HttpContext 的处理
  public delegate Task RequestDelegate(HttpContext context);
}

接着让我们看一下应用程序里面是怎么让它工作的?

之前课程讲过 Configure 方法是用来注册中间件的

app.UseMyMiddleware();

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
});

根据刚才流程图表示的话,实际上中间件的执行顺序是跟注册顺序有关系的,最早注册的中间件它的权力是最大的,它可以越早的发生作用

中间件的注册实际上不仅仅是有上面展示的已有内置的中间件,实际上还可以用注册委托的方法来注册我们的逻辑

app.Use(async (context, next) =>
{
    await context.Response.WriteAsync("Hello");
});

因为这个中间件注册最早,而且不对后续的 next 做任何操作,所以启动之后无论输入什么都会输出 Hello

如果需要后续的中间件执行,那就意味着需要调用 next,可以在中间件执行之后再次 Hello 一次

app.Use(async (context, next) =>
{
    await context.Response.WriteAsync("Hello");
    await next();
    await context.Response.WriteAsync("Hello2");
});

启动程序报错:

System.InvalidOperationException: Headers are read-only, response has already started.

意味着一旦应用程序已经对 Response 输出内容,我们就不能对 header 进行操作了,但是可以在 Response 后续继续写出信息

app.Use(async (context, next) =>
{
    //await context.Response.WriteAsync("Hello");
    await next();
    await context.Response.WriteAsync("Hello2");
});

实际上除了 Use 这种方式的话,还有 Map 的方式

app.Map("/abc", abcBuilder =>
{
    abcBuilder.Use(async (context, next) =>
    {
        //await context.Response.WriteAsync("Hello");
        await next();
        await context.Response.WriteAsync("Hello2");
    });
});

启动程序不会直接看到 Hello 输出,如果把地址改为 localhost:5001/abc,我们的输出就会变成 Hello2

也就是说当我们需要对特定的路径进行指定中间件的时候可以这样做

如果在 Map 的时候逻辑复杂一点,不仅仅判断它的 URL 地址,而且要做特殊的判断的话,可以这么做把判断逻辑变成一个委托

我们要判断当我们的请求地址包含 abc 的时候,输出 new abc

app.MapWhen(context =>
{
    return context.Request.Query.Keys.Contains("abc");
}, builder =>
{
    builder.Run(async context =>
    {
        await context.Response.WriteAsync("new abc");
    });
});

启动程序,没有任何输出

当我们在默认启动地址后面输入 ?abc=1 的时候,可以看到输出了 new abc

这里用到了一个 Run 的方法,上一节用到的是 Use 方法

app.Map("/abc", abcBuilder =>
{
    abcBuilder.Use(async (context, next) =>
    {
        //await context.Response.WriteAsync("Hello");
        await next();
        await context.Response.WriteAsync("Hello2");
    });
});

Run 和 Use 的区别是什么呢?

Use 是指我们可以像注册一个完整的中间件一样,将 next 注入进来,我们可以去决定是否执行后续的中间件

Run 的含义就表示我们这里就是中间件执行的末端,也就不在执行后面的中间件了,在这里将返回请求

那我们如何像 UseRouting UseEndpoints 一样来设计我们自己的中间件呢?

这里定义好了一个 MyMiddleware

namespace MiddlewareDemo.Middlewares
{
    class MyMiddleware
    {
        RequestDelegate _next;
        ILogger _logger;
        public MyMiddleware(RequestDelegate next, ILogger<MyMiddleware> logger)
        {
            _next = next;
            _logger = logger;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            using (_logger.BeginScope("TraceIdentifier:{TraceIdentifier}", context.TraceIdentifier))
            {
                _logger.LogDebug("开始执行");
                
                await _next(context);

                _logger.LogDebug("执行结束");
            }
        }
    }
}

定义中间件是用了一个约定的方式,中间件的类包含一个方法 Invoke 或者 InvokeAsync 这样一个方法,它的返回是一个 Task,入参是一个 HttpContext,实际上可以理解成与中间件的委托是一样的,只要我们的类包含这样一个方法,就可以把它作为一个中间件注册进去,并被框架识别到

这里还定义了一个 MyBuilderExtensions

namespace Microsoft.AspNetCore.Builder
{
    public static class MyBuilderExtensions
    {
        public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder app)
        {
            return app.UseMiddleware<MyMiddleware>();
        }
    }
}

把我们的中间件注册进去,这个方法就是 UseMyMiddleware

通过这样的定义,我们就可以使用自己的中间件

app.UseMyMiddleware();

启动程序,输出如下:

控制台输出

dbug: MiddlewareDemo.Middlewares.MyMiddleware[0]
      => RequestPath:/weatherforecast RequestId:0HLU50UEM3M9F:00000001, SpanId:|77f92fe8-4a6d800968327989., TraceId:77f92fe8-4a6d800968327989, ParentId: => TraceIdentifier:0HLU50UEM3M9F:00000001
      开始执行
dbug: MiddlewareDemo.Middlewares.MyMiddleware[0]
      => RequestPath:/weatherforecast RequestId:0HLU50UEM3M9F:00000001, SpanId:|77f92fe8-4a6d800968327989., TraceId:77f92fe8-4a6d800968327989, ParentId: => TraceIdentifier:0HLU50UEM3M9F:00000001
      执行结束

网页控制器输出

[{"date":"2020-03-11T23:30:55.3411696+08:00","temperatureC":20,"temperatureF":67,"summary":"Warm"},{"date":"2020-03-12T23:30:55.3417863+08:00","temperatureC":52,"temperatureF":125,"summary":"Bracing"},{"date":"2020-03-13T23:30:55.3417916+08:00","temperatureC":-3,"temperatureF":27,"summary":"Mild"},{"date":"2020-03-14T23:30:55.341792+08:00","temperatureC":35,"temperatureF":94,"summary":"Balmy"},{"date":"2020-03-15T23:30:55.3417923+08:00","temperatureC":37,"temperatureF":98,"summary":"Sweltering"}]Hello2

如果要实现一个断路器,就是不执行后续逻辑,注释掉一行

_logger.LogDebug("开始执行");

//await _next(context);

_logger.LogDebug("执行结束");

启动程序,页面不会输出任何内容,只会在控制台打印出中间件的执行过程,后续的控制器不会执行

这样就实现了一个断路器,也就意味着可以使用自己的中间件做请求的控制,而且时非常灵活的控制

在使用中间件的过程中,需要非常注意的是注册中间件的顺序,这些顺序就决定了中间件执行的时机,某些中间件会是断路器的作用,某些中间件会做一些请求内容的处理

还有一个比较关键的要点是指应用程序一旦开始向 Response write 的时候,后续的中间件就不能再去操作它的 header,这一点是需要注意的

可以通过 Context.Response.HasStarted 来判断是否已经开始向响应的 body 输出内容,一旦输出了内容,就不要再操作 header

GitHub源码链接:

https://github.com/MingsonZheng/DotNetCoreDevelopmentActualCombat/tree/main/MiddlewareDemo