Asp.Net Core WebApi学习笔记(四)-- Middleware

本文记录了Asp.Net管道模型和Asp.Net Core的Middleware模型的对比,并在上一篇的基础上增加Middleware功能支持。

在演示Middleware功能之前,先要了解一下Asp.Net管道模型发生了什么样的变化。

第一部分:管道模型

1. Asp.Net管道

在之前的Asp.Net里,主要的管道模型流程如下图所示:

请求进入Asp.Net工作进程后,由进程创建HttpWorkRequest对象,封装此次请求有关的所有信息,然后进入HttpRuntime类进行进一步处理。HttpRuntime通过请求信息创建HttpContext上下文对象,此对象将贯穿整个管道,直到响应结束。同时创建或从应用程序池里初始化一个HttpApplication对象,由此对象开始处理之前注册的多个HttpModule。之后调用HandlerFactory创建Handler处理程序,最终处理此次请求内容,生成响应返回。

下面用一个简单的Asp.Net程序来验证这个流程。

使用VS2015创建一个空的Asp.Net项目,根据向导添加HttpModule.cs、HttpHandler.cs、Global.asax文件

 HttpModule.cs
 HttpHandler.cs

配置Web.Config。以下是在IIS7环境下的配置内容。

 1 <?xml version="1.0" encoding="utf-8"?>
2 <!--
3 有关如何配置 ASP.NET 应用程序的详细信息,请访问
4 http://go.microsoft.com/fwlink/?LinkId=169433
5 -->
6 <configuration>
7 <system.web>
8 <compilation debug="true" targetFramework="4.5"/>
9 <httpRuntime targetFramework="4.5"/>
10 </system.web>
11 <system.webServer>
12 <validation validateIntegratedModeConfiguration="false"/>
13 <handlers>
14 <add name="handler" verb="GET" path="index.handler" type="WebApplicationTest.HttpHandler,WebApplicationTest"/>
15 </handlers>
16 <modules>
17 <add name="module1" type="WebApplicationTest.HttpModule1,WebApplicationTest"/>
18 <add name="module2" type="WebApplicationTest.HttpModule2,WebApplicationTest"/>
19 <add name="module3" type="WebApplicationTest.HttpModule3,WebApplicationTest"/>
20 </modules>
21 </system.webServer>
22 </configuration>

启动调试,访问地址 http://localhost:5383/index.handler ,可以看到页面内容。

之前版本的Asp.Net MVC正是通过 UrlRoutingModule.cs 类和 MvcHandler.cs 类进行扩展从而实现了MVC框架。

2、Asp.Net Core管道

而在Asp.Net Core里面,管道模型流程发生了很大的变化:

IHttpModule和IHttpHandler不复存在,取而代之的是一个个中间件(Middleware)。

Server将接收到的请求直接向后传递,依次经过每一个中间件进行处理,然后由最后一个中间件处理并生成响应内容后回传,再反向依次经过每个中间件,直到由Server发送出去。

中间件就像一层一层的“滤网”,过滤所有的请求和相应。这一设计非常适用于“请求-响应”这样的场景——消息从管道头流入最后反向流出。

接下来将演示在Asp.Net Core里如何实现中间件功能。

第二部分、Middleware

其实,在这个系列的第一篇里面,已经展示了管道的一个简单用法。这里再详细讲解一下如何实现自定义管道。

Middleware支持Run、Use和Map三种方法进行注册,下面将展示每一种方法的使用方式。

一、Run方法

所有需要实现的自定义管道都要在 Startup.cs 的 Configure 方法里添加注册。

 1         public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
2 {
3 // 添加日志支持
4 loggerFactory.AddConsole();
5 loggerFactory.AddDebug();
6
7 // 添加NLog日志支持
8 loggerFactory.AddNLog();
9
10 // 添加自定义中间件
11 app.Run(async context =>
12 {
13 await context.Response.WriteAsync("Hello World!");
14 });
15
16 // 添加MVC中间件
17 //app.UseMvc();
18 }

启动调试,访问地址 http://localhost:5000/ ,页面显示Hello World!字样。

再次添加一个Run方法

 1         public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
2 {
3 // 添加日志支持
4 loggerFactory.AddConsole();
5 loggerFactory.AddDebug();
6
7 // 添加NLog日志支持
8 loggerFactory.AddNLog();
9
10 // 添加自定义中间件
11 app.Run(async context =>
12 {
13 await context.Response.WriteAsync("Hello World!");
14 });
15
16 app.Run(async context =>
17 {
18 await context.Response.WriteAsync("Hello World too!");
19 });
20
21 // 添加MVC中间件
22 //app.UseMvc();
23 }

启动调试,再次访问发现页面上只有Hello World!字样。

原因是:Run的这种用法表示注册的此中间件为管道内的最后一个中间件,由它处理完请求后直接返回。

二、Use方法

 1         public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
2 {
3 // 添加日志支持
4 loggerFactory.AddConsole();
5 loggerFactory.AddDebug();
6
7 // 添加NLog日志支持
8 loggerFactory.AddNLog();
9
10 // 添加自定义中间件
11 app.Use(async (context, next) =>
12 {
13 await context.Response.WriteAsync("Hello World!");
14 });
15
16 // 添加MVC中间件
17 //app.UseMvc();
18 }

启动调试,访问页面同样显示Hello World!字样。我们发现使用Use方法替代Run方法,一样可以实现同样的功能。

再次添加一个Use方法,将原来的Use方法内容稍作调整,尝试实现页面显示两个Hello World!字样。

 1         public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
2 {
3 // 添加日志支持
4 loggerFactory.AddConsole();
5 loggerFactory.AddDebug();
6
7 // 添加NLog日志支持
8 loggerFactory.AddNLog();
9
10 // 添加自定义中间件
11 app.Use(async (context, next) =>
12 {
13 await context.Response.WriteAsync("Hello World!");
14 await next();
15 });
16
17 app.Use(async (context, next) =>
18 {
19 await context.Response.WriteAsync("Hello World too!");
20 });
21
22 // 添加MVC中间件
23 //app.UseMvc();
24 }

启动调试,访问页面

将两个Use方法换个顺序,稍微调整一下内容,再次启动调试,访问页面,发现字样输出顺序也发生了变化。

 1         public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
2 {
3 // 添加日志支持
4 loggerFactory.AddConsole();
5 loggerFactory.AddDebug();
6
7 // 添加NLog日志支持
8 loggerFactory.AddNLog(); HelloworldMiddleware.cs 
9
10 // 添加自定义中间件
11 app.Use(async (context, next) =>
12 {
13 await context.Response.WriteAsync("Hello World too!");
14 await next();
15 });
16
17 app.Use(async (context, next) =>
18 {
19 await context.Response.WriteAsync("Hello World!");
20 });
21
22 // 添加MVC中间件
23 //app.UseMvc();
24 }

从上面的例子可以发现,通过Use方法注册的中间件,如果不调用next方法,效果等同于Run方法。当调用next方法后,此中间件处理完后将请求传递下去,由后续的中间件继续处理。

当注册中间件顺序不一样时,处理的顺序也不一样,这一点很重要,当注册的自定义中间件数量较多时,需要考虑哪些中间件先处理请求,哪些中间件后处理请求。

另外,我们可以将中间件单独写成独立的类,通过UseMiddleware方法同样可以完成注册。下面将通过独立的中间件类重写上面的演示功能。

新建两个中间件类: HelloworldMiddleware.cs 、 HelloworldTooMiddleware.cs

 HelloworldMiddleware.cs
 HelloworldTooMiddleware.cs

修改 Startup.cs 的Configure方法内容

 1         public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
2 {
3 // 添加日志支持
4 loggerFactory.AddConsole();
5 loggerFactory.AddDebug();
6
7 // 添加NLog日志支持
8 loggerFactory.AddNLog();
9
10 // 添加自定义中间件
11 app.UseMiddleware<HelloworldMiddleware>();
12 app.UseMiddleware<HelloworldTooMiddleware>();
13
14 // 添加MVC中间件
15 //app.UseMvc();
16 }

启动调试,访问页面,可以看到同样的效果。

三、Map方法

Map方法主要通过请求路径和其他自定义条件过滤来指定注册的中间件,看起来更像一个路由。

修改 Startup.cs 的Configure方法内容,增加静态方法MapTest

 1         public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
2 {
3 // 添加日志支持
4 loggerFactory.AddConsole();
5 loggerFactory.AddDebug();
6
7 // 添加NLog日志支持
8 loggerFactory.AddNLog();
9
10 // 添加自定义中间件
11 app.Map("/test", MapTest);
12
13 // 添加MVC中间件
14 //app.UseMvc();
15 }
16
17 private static void MapTest(IApplicationBuilder app){
18 app.Run(async context => {
19 await context.Response.WriteAsync("Url is " + context.Request.PathBase.ToString());
20 });
21 }

启动调试,访问路径 http://localhost:5000/test ,页面显示如下内容

但是访问其他路径时,页面没有内容显示。从这个可以看到,Map方法通过类似路由的机制,将特定的Url地址请求引导到固定的方法里,由特定的中间件处理。

另外,Map方法还可以实现多级Url“路由”,其实就是Map方法的嵌套使用

 1             // 添加自定义中间件
2 app.Map("/level1", lv1App => {
3 app.Map("/level1.1", lv11App => {
4 // /level1/level1.1
5
6 });
7
8 app.Map("/level1.2", lv12App => {
9 // /level1/level1.2
10
11 });
12 });

也可以通过MapWhen方法使用自定义条件进行“路由”

 1         public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
2 {
3 // 添加日志支持
4 loggerFactory.AddConsole();
5 loggerFactory.AddDebug();
6
7 // 添加NLog日志支持
8 loggerFactory.AddNLog();
9
10 // 添加自定义中间件
11 app.MapWhen(context =>
12 {
13 return context.Request.Query.ContainsKey("a");
14 }, MapTest);
15
16 // 添加MVC中间件
17 //app.UseMvc();
18 }
19
20 private static void MapTest(IApplicationBuilder app)
21 {
22 app.Run(async context =>
23 {
24 await context.Response.WriteAsync($"Url is {context.Request.Path.ToString()}{context.Request.QueryString.Value}");
25 });
26
27 }

启动调试,访问路径 http://localhost:5000/path?a=1&b=2 ,页面显示如下内容

只有当请求参数中含有a时,页面才正常显示内容。

四、其他内置的中间件

Asp.Net Core框架内置了几个中间件

最后,用自定义中间件实现一个简单的访问日志记录功能,记录每一次请求的内容和响应时间。

1. 添加日志模型 VisitLog.cs

 1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4
5 namespace WebApiFrame.Models
6 {
7 public class VisitLog
8 {
9 public string Url { get; set; }
10
11 public IDictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
12
13 public string Method { get; set; }
14
15 public string RequestBody { get; set; }
16
17 public DateTime ExcuteStartTime { get; set; }
18
19 public DateTime ExcuteEndTime { get; set; }
20
21 public override string ToString()
22 {
23 string headers = "[" + string.Join(",", this.Headers.Select(i => "{" + $"\"{i.Key}\":\"{i.Value}\"" + "}")) + "]";
24 return $"Url: {this.Url},\r\nHeaders: {headers},\r\nMethod: {this.Method},\r\nRequestBody: {this.RequestBody},\r\nExcuteStartTime: {this.ExcuteStartTime.ToString("yyyy-MM-dd HH:mm:ss.fff")},\r\nExcuteStartTime: {this.ExcuteEndTime.ToString("yyyy-MM-dd HH:mm:ss.fff")}";
25 }
26 }
27 }

2. 添加访问日志记录中间件 VisitLogMiddleware.cs ,同时添加UseVisitLogger扩展方法。

 1 using Microsoft.AspNetCore.Builder;
2 using Microsoft.AspNetCore.Http;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.IO;
6 using System.Linq;
7 using System.Threading.Tasks;
8 using WebApiFrame.Models;
9
10 namespace WebApiFrame.Core.Middlewares
11 {
12 public class VisitLogMiddleware
13 {
14 private readonly RequestDelegate _next;
15
16 private readonly ILogger logger;
17
18 private VisitLog visitLog;
19
20 public VisitLogMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
21 {
22 _next = next;
23 logger = loggerFactory.CreateLogger<VisitLogMiddleware>();
24 }
25
26 public async Task Invoke(HttpContext context)
27 {
28 visitLog = new VisitLog();
29 HttpRequest request = context.Request;
30 visitLog.Url = request.Path.ToString();
31 visitLog.Headers = request.Headers.ToDictionary(k => k.Key, v => string.Join(";", v.Value.ToList()));
32 visitLog.Method = request.Method;
33 visitLog.ExcuteStartTime = DateTime.Now;
34
35 using (StreamReader reader = new StreamReader(request.Body))
36 {
37 visitLog.RequestBody = reader.ReadToEnd();
38 }
39
40 context.Response.OnCompleted(ResponseCompletedCallback, context);
41 await _next(context);
42 }
43
44 private Task ResponseCompletedCallback(object obj)
45 {
46 visitLog.ExcuteEndTime = DateTime.Now;
47 logger.LogInformation($"VisitLog: {visitLog.ToString()}");
48 return Task.FromResult(0);
49 }
50 }
51
52 public static class VisitLogMiddlewareExtensions
53 {
54 public static IApplicationBuilder UseVisitLogger(this IApplicationBuilder builder)
55 {
56 return builder.UseMiddleware<VisitLogMiddleware>();
57 }
58 }
59 }

3. 在 Startup.cs 添加中间件支持

 1         public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
2 {
3 // 添加日志支持
4 loggerFactory.AddConsole();
5 loggerFactory.AddDebug();
6
7 // 添加NLog日志支持
8 loggerFactory.AddNLog();
9
10 // 添加自定义中间件
11 app.UseVisitLogger();
12
13 app.Run(async context =>
14 {
15 await context.Response.WriteAsync("Hello World!");
16 });
17
18
19 // 添加MVC中间件
20 //app.UseMvc();
21 }

4. 启动调试,访问地址 http://localhost:5000/ ,查看调试控制台日志打印信息。

另外,如果你比较细心会发现,在Configure方法里有这样一句代码: app.UseMvc(); ,Asp.Net Core Mvc正是通过这个方法借用中间件来扩展实现了MVC框架。

 
分类: Asp.Net Core

Asp.Net Core WebApi学习笔记(四)-- Middleware的更多相关文章

  1. 使用Visual Studio Code开发Asp.Net Core WebApi学习笔记(四)-- Middleware

    本文记录了Asp.Net管道模型和Asp.Net Core的Middleware模型的对比,并在上一篇的基础上增加Middleware功能支持. 在演示Middleware功能之前,先要了解一下Asp ...

  2. 使用Visual Studio Code开发Asp.Net Core WebApi学习笔记(九)-- 单元测试

    本篇将结合这个系列的例子的基础上演示在Asp.Net Core里如何使用XUnit结合Moq进行单元测试,同时对整个项目进行集成测试. 第一部分.XUnit 修改 Project.json 文件内容, ...

  3. 使用Visual Studio Code开发Asp.Net Core WebApi学习笔记(七)-- 结构化配置

    本篇将记录.Net Core里颇有特色的结构化配置的使用方法. 相比较之前通过Web.Config或者App.Config配置文件里使用xml节点定义配置内容的方式,.Net Core在配置系统上发生 ...

  4. 使用Visual Studio Code开发Asp.Net Core WebApi学习笔记(六)-- 依赖注入

    本篇将介绍Asp.Net Core中一个非常重要的特性:依赖注入,并展示其简单用法. 第一部分.概念介绍 Dependency Injection:又称依赖注入,简称DI.在以前的开发方式中,层与层之 ...

  5. 使用Visual Studio Code开发Asp.Net Core WebApi学习笔记(五)-- Filter

    在上一篇里,介绍了中间件的相关内容和使用方法.本篇将介绍Asp.Net Core MVC框架的过滤器的相关内容和使用方法,并简单说明一下与中间件的区别. 第一部分.MVC框架内置过滤器 下图展示了As ...

  6. 使用Visual Studio Code开发Asp.Net Core WebApi学习笔记(十)-- 发布(Windows)

    本篇将在这个系列演示的例子上继续记录Asp.Net Core在Windows上发布的过程. Asp.Net Core在Windows上可以采用两种运行方式.一种是自托管运行,另一种是发布到IIS托管运 ...

  7. 使用Visual Studio Code开发Asp.Net Core WebApi学习笔记(三)-- Logger

    本篇是在上一篇的基础上添加日志功能,并记录NLog在Asp.Net Core里的使用方法. 第一部分:默认Logger支持 一.project.json添加日志包引用,并在cmd窗口使用 dotnet ...

  8. 使用Visual Studio Code开发Asp.Net Core WebApi学习笔记(二)-- Web Api Demo

    在上一篇里,我已经建立了一个简单的Web-Demo应用程序.这一篇将记录将此Demo程序改造成一个Web Api应用程序. 一.添加ASP.NET Core MVC包 1. 在project.json ...

  9. [转]使用Visual Studio Code开发Asp.Net Core WebApi学习笔记(三)-- Logger

    本文转自:https://www.cnblogs.com/niklai/p/5662094.html 本篇是在上一篇的基础上添加日志功能,并记录NLog在Asp.Net Core里的使用方法. 第一部 ...

随机推荐

  1. 【PRINCE2是什么】PRINCE2认证之七大原则(2)

    我们先来回顾一下,PRINCE2七大原则分别是持续的业务验证,经验学习,角色与责任,按阶段管理,例外管理,关注产品,剪裁. 第二个原则:吸取经验教训. PRINCE2要求项目团队吸取以前的经验教训,在 ...

  2. iOS中的round/ceil/floorf函数略解

    extern float ceilf(float); extern double ceil(double); extern long double ceill(long double); extern ...

  3. SELENIUM2 使用JavascriptExecutor在页面Javascipt执行

    目的: 1. 执行一段JS,来改变HTML2. 一些非标准控件无法用selenium2的API时,可以执行JS的办法来取代 主要操作:JavascriptExecutor j = (Javascrip ...

  4. [MISS静IOS开发原创文摘]-AppDelegate存储全局变量和 NSUserDefaults standardUserDefaults 通过模型保存和读取数据,存储自定义的对象

    由于app开发的需求,需要从api接口获得json格式数据并保存临时的 app的主题颜色 和 相关url 方案有很多种: 1, 通过AppDelegate保存为全局变量,再获取 2,使用NSUSerD ...

  5. intellij idea 如何更改编辑器文本字体和大小

    换上了intellij idea之后,第一件事就是想要改变下文字字体,因为在我这个27寸的2k分辨率的屏幕上,文字显然太小了. intellij idea字体设值分成两部分,一部分是UI部分字体字号设 ...

  6. 开篇呀,恭喜恭喜,是个好开头-----关于sort()排序

    感觉自己活了半辈子从来没写过博客,这可是头一回,而且不是记事是为了学习,先恭喜恭喜自己,有一个很好的开端,不管能不能半途而废,反正是想着为了学习做点什么. 之前有很多东西需要搬过来,循序渐进吧,反正也 ...

  7. StringBuffer类总结

    package day13; /* StringBuffer是字符串缓冲区. 是一个容器. 特点: 1,长度是可变化的. 2,可以字节操作多个数据类型. 3,最终会通过toString方法变成字符串. ...

  8. pstree 命令详解

    作用: 以命令树状图的方式展现进程之间的派生关系, 显示效果比较直观. 选项: -a 显示每个程序的完整指令, 包含路径, 参数或者是常驻服务的标志 -c 不使用精简标示法 -h 列出树状图,特别标明 ...

  9. BZOJ 1801: [Ahoi2009]chess 中国象棋 [DP 组合计数]

    http://www.lydsy.com/JudgeOnline/problem.php?id=1801 在N行M列的棋盘上,放若干个炮可以是0个,使得没有任何一个炮可以攻击另一个炮. 请问有多少种放 ...

  10. XXL-JOB原理--定时任务框架简介(一)

    https://blog.csdn.net/qq924862077/article/details/82595948 https://blog.csdn.net/qq924862077/article ...