C# Web开发教程(八)中间件

中间件

- 广义: ASP.NETCore中的中间件指ASP.NETCore中的一个组件。

- 组成部分: 中间件由前逻辑、next、后逻辑3部分组成,前逻辑为第一段要执行的逻辑代码、next为指向下一个中间件的调用、   后逻辑为从下一个中间件执行返回所执行的逻辑代码。每个HTTP请求都要经历一系列中间件的处理,每个中间件对于请求进行特   定的处理后,再转到下一个中间件,最终的业务逻辑代码执行完成后,响应的内容也会按照处理的相反顺序进行处理,然后形成   HTTP响应报文返回给客户端。
    
- 中间件组成一个管道,整个ASP.NETCore的执行过程就是HTTP请求和响应按照中间件组装的顺序在中间件之间流转的过程。
  开发人员可以对组成管道的中间件按照需要进行自由组合。
  • 中间件的三个概念: Map,Use,Run
- Map用来定义一个管道可以处理哪些请求
- Use和Run用来定义管道,一个管道由若干个Use和一个Run组成,每个Use引入一个中间件,
  而Run是用来执行最终的核心应用逻辑。
  • 我们创建一个空asp.net项目,从一无所有开始搭建中间件
// Program.cs(很简单的代码,跑起来只有一个Hello World!)

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

- 运行: https://localhost:7118/

- 我们可以小改一下,返回简单字符
......
app.MapGet("/", () => "Hello World!");
app.MapGet("/test", () => "你若xxxx");
......

  • 来一个简单示例
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");
//app.MapGet("/test", () => "你若xxxx");

app.Map("/test", async (pipeBuilder) =>
{
    pipeBuilder.Use(async (context, next) =>
    {
        context.Response.ContentType = "text/html";
        await context.Response.WriteAsync("1 start<br/>");
        await next.Invoke();
        await context.Response.WriteAsync("1 end<br/>");
    });

    pipeBuilder.Use(async (context, next) =>
    {
       
        await context.Response.WriteAsync("2 start<br/>");
        await next.Invoke();
        await context.Response.WriteAsync("2 end<br/>");
    });

    pipeBuilder.Use(async (context, next) =>
    {
       
        await context.Response.WriteAsync("3 start<br/>");
        await next.Invoke();
        await context.Response.WriteAsync("3 end<br/>");
    });

    pipeBuilder.Run(async context =>
    {
        await context.Response.WriteAsync("Run<br/>");
    });

});

app.Run();

自定义中间件管道(重点)

app.Map("/test", async (pipeBuilder) =>
{
    // 创建自定义的中间件管道
});

中间件管道详细解析

管道执行顺序:

请求 → 中间件1 → 中间件2 → 中间件3 → Run终端 → 中间件3 → 中间件2 → 中间件1 → 响应

三个中间件的执行流程:

pipeBuilder.Use(async (context, next) =>
{
    context.Response.ContentType = "text/html";
    await context.Response.WriteAsync("1 start<br/>");  // 1. 进入时执行
    await next.Invoke();                                // 2. 调用下一个中间件
    await context.Response.WriteAsync("1 end<br/>");    // 7. 返回时执行
});

pipeBuilder.Use(async (context, next) =>
{
    
    await context.Response.WriteAsync("2 start<br/>");  // 3. 进入时执行
    await next.Invoke();                                // 4. 调用下一个中间件
    await context.Response.WriteAsync("2 end<br/>");    // 6. 返回时执行
});

pipeBuilder.Use(async (context, next) =>
{
   
    await context.Response.WriteAsync("3 start<br/>");  // 5. 进入时执行
    await next.Invoke();                                // 调用Run(没有下一个中间件)
    await context.Response.WriteAsync("3 end<br/>");    
});

终端中间件

pipeBuilder.Run(async context =>
{
    await context.Response.WriteAsync("Run<br/>");      // 5.5 执行并终止管道
});

实际输出结果

访问 /test 路径时,浏览器会显示:

1 start
2 start
3 start
Run
3 end
2 end
1 end

关键概念说明

  1. Use() - 添加中间件,可以调用下一个中间件
  2. Run() - 终端中间件,终止管道传递
  3. next.Invoke() - 调用管道中的下一个中间件
  4. 洋葱模型 - 中间件的执行顺序:进入时正序,返回时逆序

这个示例很好地展示了ASP.NET Core中间件管道的执行原理和"洋葱模型"的工作方式。

  • 注意事项: 此时若执行Run()以后,依然运行pipeBuilder.Use是不会生效了

    • 通俗理解,相当于已经return了,那么return后面再写语句,肯定是不会执行的
    ......
    
     pipeBuilder.Run(async context =>
     {
         await context.Response.WriteAsync("Run<br/>");
     });
     // 不会执行
     pipeBuilder.Use(async (context, next) =>
     {
    
         await context.Response.WriteAsync("4 start<br/>");
         await next.Invoke();
         await context.Response.WriteAsync("4 end<br/>");
     });
    

简单的自定义中间件

  • 如果中间件的代码比较复杂,或者我们需要重复使用一个中间件的话,我们最好把中间件的代码放到一个单独的“中间件类”中。
- 中间件类是一个普通的.NET类,它不需要继承任何父类或者实现任何接口,但是这个类需要有一个构造方法,构造方法至少要有   一个RequestDelegate类型的参数,这个参数用来指向下一个中间件。这个类还需要定义一个名字为Invoke或InvokeAsync的   方法,方法至少有一个HttpContext类型的参数,方法的返回值必须是Task类型。中间件类的构造方法和lnvoke(或           lnvokeAsync)方法还可以定义其他参数,其他参数的值会通过依赖注入自动赋值。

// Test1Middleware.cs

namespace WebApplicationAboutMiddleWare
{
    public class Test1Middleware
    {
        private readonly RequestDelegate next;

        public Test1Middleware(RequestDelegate next)
        {
            this.next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            await context.Response.WriteAsync("I'm in Test1Middleware: start...<br/>");
            await next.Invoke(context);
            await context.Response.WriteAsync("I'm in Test1Middleware: end...<br/>");
        }

    }
}

// Program.cs

......
app.Map("/test", async (pipeBuilder) =>
{
    pipeBuilder.Use(async (context, next) =>
    {
        context.Response.ContentType = "text/html";
        await context.Response.WriteAsync("1 start<br/>");
        await next.Invoke();
        await context.Response.WriteAsync("1 end<br/>");
    });

    pipeBuilder.Use(async (context, next) =>
    {
       
        await context.Response.WriteAsync("2 start<br/>");
        await next.Invoke();
        await context.Response.WriteAsync("2 end<br/>");
    });

    pipeBuilder.Use(async (context, next) =>
    {
       
        await context.Response.WriteAsync("3 start<br/>");
        await next.Invoke();
        await context.Response.WriteAsync("3 end<br/>");
    });
	
	// 应用
    pipeBuilder.UseMiddleware<Test1Middleware>();

    pipeBuilder.Run(async context =>
    {
        await context.Response.WriteAsync("Run<br/>");
    });



});

app.Run();

- 测试: https://localhost:7118/test,返回结果

1 start
2 start
3 start
I'm in Test1Middleware: start...
Run
I'm in Test1Middleware: end...
3 end
2 end
1 end

理解自定义MVC框架原理

  • 项目地址
- https://github.com/yangzhongke/NETBookMaterials/tree/main/%E7%AC%AC%E4%B8%83%E7%AB%A0/MiniWebAPIDemo1
- https://github.com/yangzhongke/NETBookMaterials/tree/main/%E7%AC%AC%E4%B8%83%E7%AB%A0/MiniWebAPI
  • 访问接口
- 未知地址: 重定向到默认页
- https://localhost:7195/index.html
- https://localhost:7195/mvp.png
- https://localhost:7195/test1/get2
- https://localhost:7195/test1/incage // 使用post 传一个json对象过去~~~
首先看第一个项目 MiniWebAPIDemo1,里面有 Controllers(控制器)、Models(模型)、Properties、wwwroot,还有 MyActionFilter1.cs(动作过滤器)、Program.cs(入口)、配置文件。这明显是一个基于 MiniWebAPI 框架的演示项目,也就是应用层,用框架来构建 API 的示例。

然后是第二个项目 MiniWebAPI,这应该是核心框架层,里面有 ActionFilters.cs(动作过滤器)、ActionLocator.cs(动作定位器)、BindingHelper.cs(绑定助手)、ContentTypeHelper.cs(内容类型助手)、MyStaticFilesMiddleware(静态文件中间件)、MyWebAPIMiddleware(WebAPI 中间件)、NotFoundMiddleware(404 中间件)、PathParser.cs(路径解析器)、项目文件。这些都是框架的核心组件,负责请求处理、路由、过滤、绑定等核心功能。

接下来要梳理组件间的流程:请求进来首先经过中间件链,先看是不是静态文件请求,用 MyStaticFilesMiddleware 处理;如果是 API 请求,交给 MyWebAPIMiddleware;如果都处理不了,NotFoundMiddleware 返回 404。在 MyWebAPIMiddleware 内部,会用 PathParser 解析路径,ActionLocator 找到对应的控制器动作,BindingHelper 处理参数绑定,ContentTypeHelper 处理内容类型(比如请求响应的 JSON 等),ActionFilters 处理动作过滤(比如 Demo 里的 MyActionFilter1 会用到框架的 ActionFilters 基础)。然后 Demo 项目的 Controllers 依赖框架的动作定位和过滤,Models 是数据模型,配置文件提供配置支持,wwwroot 是静态资源。

然后用 Mermaid 的 flowchart 或者 componentDiagram 来画,分层:客户端层、中间件层(框架核心)、框架核心组件层、应用层(Demo 项目)、基础支持层(配置、静态资源)。流程上要体现请求的流转顺序,组件间的依赖关系。
需要注意区分框架层和应用层,避免混淆。然后把每个组件的作用在图里或者说明里体现出来。比如 MyWebAPIMiddleware 是核心中间件,协调其他核心组件;ActionLocator 负责找到对应的 Controller 和 Action;BindingHelper 把请求参数绑定到 Action 参数上,等等。

现在组织 Mermaid 代码,用 flowchart TD,分层次,从请求开始,经过中间件,到框架核心组件,再到应用层的 Controllers 和 Models,最后返回响应。同时加上注释说明每个组件的作用,以及两个项目(框架核心 MiniWebAPI 和应用 Demo MiniWebAPIDemo1)的对应关系。
### 框架原理图说明
1. **项目角色区分**
   - **MiniWebAPI项目**(框架核心):提供中间件、路由解析、参数绑定等底层能力,是应用开发的基础依赖
   - **MiniWebAPIDemo1项目**(应用示例):基于框架开发的具体API应用,包含业务控制器、模型和自定义过滤器

2. **核心请求流程**
   1. 客户端发起请求,先进入`MyStaticFilesMiddleware`判断是否为静态资源(如wwwroot下的文件)
   2. 非静态资源请求进入`MyWebAPIMiddleware`(API核心),通过`PathParser`解析URL
   3. `ActionLocator`根据解析结果找到对应的应用控制器(Controllers)和动作方法
   4. `BindingHelper`结合`ContentTypeHelper`处理请求参数,绑定到动作方法的参数(依赖Models)
   5. 执行自定义过滤器(MyActionFilter1,基于框架ActionFilters抽象)和业务逻辑
   6. 未匹配的请求最终由`NotFoundMiddleware`返回404

3. **关键组件职责**
   - 中间件链:负责请求的拦截和分发,是ASP.NET Core middleware模式的实现
   - 动作定位与参数绑定:解决"如何找到目标方法"和"如何传递参数"的核心问题
   - 过滤器机制:支持业务逻辑的横向切入(如日志、权限校验),体现AOP思想
   - 静态资源处理:分离API与静态资源请求,符合Web应用的常规分层

未命名绘图-第 1 页

中间件过滤器的区别

  • 中间件是 “全流程的岗位”,过滤器是 “特定环节的专项检查”
中间件:餐厅里 “从头到尾的关键岗位”
把客户端的请求比作 “顾客去餐厅吃饭”,中间件就像餐厅里每个必经环节的工作人员,从进门到出门都能 “插手”,且顺序固定。

比如:
门口引导员(对应MyStaticFilesMiddleware):顾客一进门,先判断你是来 “取预定的蛋糕”(静态资源请求)还是 “进店吃饭”(API 请求);如果是取蛋糕,直接给你,不用往后走。
服务员(对应MyWebAPIMiddleware):引导员判断你是吃饭,就把你交给服务员,服务员会问你 “坐哪桌”“点什么菜”(解析 URL、定位控制器)。
收银台(对应NotFoundMiddleware):如果服务员发现你点的菜 “菜单上没有”(请求没匹配到 API),就交给收银台,告诉你 “不好意思,没有这道菜”(返回 404)。
核心特点:管 “全局流程”,每个中间件都能接收 “所有请求”,能决定 “请求是否继续往后传”,还能直接处理并返回响应(比如引导员直接给蛋糕)
过滤器:餐厅里 “厨房的品控员”

过滤器不像中间件那样管全流程,它只盯着 “某个特定业务环节”—— 比如餐厅里只在 “做菜” 这个环节工作的品控员,其他环节(进门、点单、收银)它都不管。
比如:你点了一道 “番茄炒蛋”(对应控制器的GetUser方法),厨房开始做这道菜时:
品控员先检查 “食材够不够”(对应过滤器的 “前置处理”,比如校验请求参数是否合法);
厨师做完后,品控员再尝一口 “盐放得对不对”(对应过滤器的 “后置处理”,比如记录接口执行时间、包装响应格式);
如果是其他环节(比如你进门、收银),品控员完全不参与。
核心特点:管 “局部环节”,只针对 “控制器的动作方法”(比如UserController.Get)工作,只能在 “动作执行前 / 后” 插手,没法拦截 “静态资源请求”“404 请求” 这类和控制器无关的流程。
  • 总结
中间件是 “餐厅门口到收银台的全流程岗”,啥请求都能碰,能拦能返;过滤器是 “只盯厨房做菜的品控岗”,只管控制器的动作,其他流程插不上手。

需求: 开发支持markdowm的中间件,将Markdown文件自动转换为HTML,以便在网页中查看

  • 新建一个MVC项目,wwwroot目录底下,放一个readme.md,访问地址: https://localhost:7075/readme.md
- 可以发现,网页中并没有支持markdown的格式,现在要解决这个问题
  
  • 安装处理markdowm的包
- Install-Package Ude.NetStandard
- Install-Package MarkdownSharp
// MarkdownMiddleware.cs

using MarkdownSharp;
using System.Text;

namespace WebApplicationAboutMarkdowm
{
    public class MarkdownMiddleware
    {
        private readonly RequestDelegate next;
        private readonly IWebHostEnvironment hostEn;
		
		 // 构造函数:接收下一个中间件和主机环境
        public MarkdownMiddleware(RequestDelegate next, IWebHostEnvironment hostEn)
        {
            this.next = next;
            this.hostEn = hostEn;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            string path = context.Request.Path.ToString();
			// 检查请求的是否是.md文件
            if (!path.EndsWith(".md", true, null))
            {
                await next.Invoke(context); // 不是md文件,交给下一个中间件
                return;
            }

			// 在wwwroot目录中查找对应的文件
            var file = hostEn.WebRootFileProvider.GetFileInfo(path);
            if (!file.Exists)
            {
                await next.Invoke(context); // 文件不存在,交给下一个中间件
                return;
            }
            // 文件读取和编码检测
            using var stream = file.CreateReadStream();
            Ude.CharsetDetector cdet = new(); // 使用Ude库检测文件编码
            cdet.Feed(stream);
            cdet.DataEnd();
            string charset = cdet.Charset ?? "UTF-8"; // 默认使用UTF-8
            stream.Position = 0; // 重置流位置
            
			// 使用检测到的编码读取文件内容
            using StreamReader reader = new(stream,Encoding.GetEncoding(charset));
            string mdText = await reader.ReadToEndAsync();

            Markdown md = new();
            string html = md.Transform(mdText); // 将Markdown转换为HTML
            context.Response.ContentType = "text/html;charset=UTF-8";
            await context.Response.WriteAsync(html); // 返回HTML内容

        }
    }
}

// Prgram.cs 
......
app.UseHttpsRedirection();
// 注册
app.UseMiddleware<MarkdownMiddleware>();
app.UseStaticFiles();

......

- 访问 https://localhost:7075/readme.md 查看效果

工作流程总结

  1. 请求拦截:只处理以.md结尾的URL请求
  2. 文件检查:在wwwroot目录中查找对应的Markdown文件
  3. 编码检测:自动检测文件编码,支持多种字符集
  4. 内容转换:使用MarkdownSharp库将Markdown转换为HTML
  5. 响应返回:以HTML格式返回转换后的内容
posted @ 2025-10-23 11:17  清安宁  阅读(2)  评论(0)    收藏  举报