ASP.NETCore学习记录(二) —— ASP.NET Core 中间件
ASP.NET Core 中间件
目录:
我们知道在 ASP.NET 中,有一个面向切面的请求管道,由22个主要的事件构成,能够让我们在往预定的执行顺序里面添加自己的处理逻辑。一般采取两种方式:一种是直接在 Global.asax 中对应的方法中直接添加代码。一种是是在 web.config 中通过注册 HttpModule 来实现对请求管道事件监听,并通过 HttpHandler 进入到我们的应用程序中。而在 ASP.NET Core 中,对请求管道进行了重新设计,通过使用一种称为中间件的方式来进行管道的注册,同时也变得更加简洁和强大。关于 HTTP请求流程和管道模型可以看这篇博客 https://www.cnblogs.com/xuhuale/p/10030878.html
什么是中间件 ?
中间件 (Middleware) 是组装到应用程序管道中以处理请求和响应的组件。 管道内的每个组件都可以选择是否将请求交给下一个组件,并在管道中调用下一个组件之前和之后执行某些操作。请求委托被用来建立请求管道,请求委托处理每一个HTTP请求。
请求委托通过使用 IApplicationBuilder 类型的 Run、Map 以及 Use 扩展方法来配置,并在 Startup 类中传给 Configure方法。每个单独的清求委托都可以被指定为一个内嵌匿名方法,或其定义在一个可重用的类中。这些可重用的类被称作 “中间件”或“中间件组件”。每个位于请求管道内的中间件组件负责调用管道中的下一个组件,或适时短路调用链。
ASP.NET请求管道由一系列的请求委托所构成,它们一个接着一个被调用,如图所示(该执行线程按黑色箭头的顺序执行)。

每个委托:
- 均可在下一个委托前后执行操作;
- 委托还可以决定是否将请求传递给下一个委托(任何委托都能选择停止传递到下一个委托,转而自己处理该请求,这就是请求管道的短路)。
短路是一种有意义的设计,因为它可以避免不必要的工作。比如说:
- 静态文件中间件可以返回静态文件请求并使管道的其余部分短路。
- 授权中间件只有通过身份验证之后才能调用下一个中间件,否则就会被他短路。
先在管道中调用异常处理委托,以便它们可以捕获在管道的后期阶段所发生的异常。
IApplicationBuilder
在笔记一中,我们就简单介绍过 IApplicationBuilder,在 Startup 类的 Configure方法中,第一个参数便是 IApplicationBuilder。
首先,IApplicationBuilder 是用来构建请求管道的,而所谓请求管道,本质上就是对 HttpContext 的一系列操作,即通过对 Request 的处理,来生成 Reponse。在 ASP.NET Core 中定义了一个 RequestDelegate 委托,来表示请求管道中的一个步骤,而对请求管道的注册是通过 Func<RequestDelegate, RequestDelegate> 类型的委托(也就是中间件)来实现的。
使用 IApplicationBuilder 创建中间件
可以先看一下 VisualStudio2017 中默认Web(MVC)站点模板关于请求管道设置的例子。

在 Startup.cs 的 Configure 方法默认增加了下列这些中间件组件:
- 异常/错误处理(同时针对开发环境和非开发环境)
- Https重定向
- 静态文件服务器
- Cookie 策略实施
- MVC
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
// 开发环境
app.UseDeveloperExceptionPage();
}
else
{
// 非开发环境
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();// Https重定向
app.UseStaticFiles(); // 静态文件
app.UseCookiePolicy(); // 使用Cookie策略
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
}); // MVC
}
上面的代码在非开发环境中,UseExceptionHandler 是第一个被加入到管道中的中间件,因此将会捕获之后调用组件中出现的任何异常,然后跳转到设置的异常页(/Home/Error)。
使用 UseHttpsRedirection 中间件,可以轻松实施HTTPS,将HTTP重定向到HTTPS(ASP.NET Core 2.1具有新功能)。
然后就是静态文件中间件 UseStaticFiles,静态文件中间件不提供授权检查,由它提供的任何文件,包括那些位于wwwroot下的文件都是公开可被访问的。UseCookiePolicy 是使用ASP.NET Core中的Cookie策略(详解Microsoft.AspNetCore.CookiePolicy),管道的最后执行的也就是MVC框架。
顺序
向 Startup.Configure 方法添加中间件组件的顺序定义了针对请求调用这些组件的顺序,以及响应的相反顺序。 此排序对于安全性、性能和功能至关重要。请求在每一步都可能被短路,所以我们要以正确的顺序添加中间件,如异常处理,我们需要添加在最开始,这样我们就能第一时间捕获异常,以及后续中间可能发生的异常,然后最终做处理返回。
最简单的ASP.NET应用程序(空白Web模板)是使用单个请求委托来处理所有请求。事实上,在这种情况下,并不存在所谓的“管道”,针对每个HTTP请求都调用单个匿名函数来处理。
public void Configure(IApplicationBuilder app)
{
// 使用单个请求委托来处理所有请求
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});
}
上方代码中 app.Run() 会中断管道,调用单个匿名函数来处理HTTP请求。在下面的例子中,Run了两个委托,只有第一个委托(“Hello,World!”)会被运行。
public void Configure(IApplicationBuilder app)
{
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});
app.Run(async (context) =>
{
await context.Response.WriteAsync("Welcome!");
});
}
通过运行项目,发现确实只有在第一个委托执行了,app.Run 终止了管道。

你可以将多个请求委托 app.Use 连接在一起,next参数表示管道内的下一个请求委托。在管道中,可以通过不调用next参数来终止或短路管道。通常可以在执行下一个委托之前和之后执行一些操作,如下例所示:
public void Configure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("Handing Request.\r\n");
// 调用管道中的下一个委托
await next.Invoke();
await context.Response.WriteAsync("Finish Handing Request.\r\n");
});
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello from Middleware\r\n");
});
}
运行项目,在浏览器中访问出现如下结果:

Run、Map 与 Use 方法
你可以使用 Run、Map 和 Use 方法配置 HTTP 管道。
Run 方法会短路管道,因为它不会调用 next 请求委托。因此 Run 方法一般只在管道底部被调用。Run 方法是一种惯例约定,有些中间件组件可能会暴露它们自己的 Run[Middleware]方法,而这些方法只能在管道末尾处运行。
Use 前面已经简单介绍通过 Use 构建请求管道的例子,Use 方法亦可使管道短路(即不调用 next 请求委托)。
Map 扩展方法用匹配基于请求路径的请求委托,Map只接受路径,并配置单独的中间件管道的功能。 Map* 方法可以基于给定请求路径的匹配项来创建请求管道分支。 如果请求路径以给定路径开头,则执行分支。如下面例子中:
public void Configure(IApplicationBuilder app)
{
app.Map("/map1", HandleMapTest1);
app.Map("/map2", HandleMapTest2);
app.Run(async context =>
{
await context.Response.WriteAsync("<p> Hello from non-Map delegate. </p>");
});
}
private static void HandleMapTest1(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("<p> Map Test 1 </p>");
});
}
private static void HandleMapTest2(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("<p> Map Test 2 </p>");
});
}
任何基于路径 /map1 的请求都会被管道中所配置 HandleMapTest1 方法处理。基于路径 /map2 的请求都会被管道中所配置 HandleMapTest2 方法处理。

下表显示上例来自 http://localhost:52831 的请求和响应。
请求 | 响应 |
---|---|
http://localhost:52831 | Hello from non-Map delegate. |
http://localhost:52831/map1 | Map Test 1 |
http://localhost:52831/map2 | Map Test 2 |
http://localhost:52831/map3 | Hello from non-Map delegate. |
除了基于路径的映射外,MapWhen 方法还支持基于谓语的中间件分支,允许以非常灵活的方式构建单独的管道。任何 Func<HttpContext, bool> 类型的谓语都可以被用于将请求映射到新的管道分支。下例中使用了一个简单的谓语来检测查洵字符串中变量 branch 是否存在:
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.MapWhen(context => context.Request.Query.ContainsKey("branch"), HandleBranch);
app.Run(async context =>
{
await context.Response.WriteAsync("<p> Hello from non-Map delegate. </p>");
});
}
private static void HandleBranch(IApplicationBuilder app)
{
app.Run(async context =>
{
var branchVer = context.Request.Query["branch"];
await context.Response.WriteAsync($"Branch used = {branchVer}");
});
}
}
使用了上面的设置后,任何包含请求字符 branch 的请求将使用定义于 HandleBranch 方法内的管道,如果没有包含查询字符串 branch 的请求,将被 app.Run 所定义的委托处理。

请求 | 响应 |
---|---|
http://localhost:52831 | Hello from non-Map delegate. |
http://localhost:52831/?branch=1 | Branch used = 1 |
http://localhost:52831/?branch=2 | Branch used = 2 |
http://localhost:52831/?branch | Branch used = |
另外还可以嵌套 Maps:
app.Map("/level1", level1App => {
level1App.Map("/level2a", level2AApp => {
// "/level1/level2a"
//...
});
level1App.Map("/level2b", level2BApp => {
// "/level1/level2b"
//...
});
});
Map 也可以一次匹配多个片段,例如:
app.Map("/level1/level2", HandleMultiSeg); // "/level1/level2"
以下 Startup.Configure 方法将为常见应用方案添加中间件组件:
中间件 | 描述 |
---|---|
身份验证(Authentication) | 提供身份验证支持 |
跨域资源共享(CORS) | 配置跨域资源共享 |
响应支持(Response Caching) | 提供缓存响应支持 |
响应压缩(Response Compression) | 提供响应压缩支持 |
路由(Routing) | 定义和约束请求路由 |
会话(Session) | 提供对管理用户会话(session)的支持 |
静态文件(Static Files) | 为静态文件和目录浏览提供服务提供支持 |
URL Rewriting Middleware | 用于重写 Url,并将请求重定向的支持 |
更多中间件组件可以到 Microsoft 文档 上查看。
实战中间件
如何实现一个中间件呢,下面我们来实际操作。
中间件遵循 显式依赖原则 ,并在其构造函数中暴露所有的依赖项。中间件能够利用 UseMiddleware<T> 扩展方法的优势,直接通过它们的构造函数注入服务。依赖注入服务是自动完成填充的,扩展所用到的 params 参数数组被用于非注入参数。
下面来实现一个记录IP的中间件。
① 新建一个 ASP.NET Core WebApplication 项目,选择空的模板。

然后为项目添加—个 Microsoft.Extensions.Logging.Console。
NuGet命令行执行(请使用官方源):
Install-Package Microsoft.Extensions.Logging.Console

② 新建一个类 RequestIPMiddleware.es,将中间件委托移动到类:
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace middlewareDemo
{
public class RequestIPMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
public RequestIPMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
{
_next = next;
_logger = loggerFactory.CreateLogger<RequestIPMiddleware>();
}
public async Task Invoke(HttpContext context)
{
_logger.LogInformation("User IP:" + context.Connection.RemoteIpAddress.ToString());
await _next.Invoke(context);
}
}
}
③ 再新建—个 RequestIPExtensions.cs,以下扩展方法通过 IApplicationBuilder 公开中间件:
using Microsoft.AspNetCore.Builder;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace middlewareDemo
{
public static class RequestIPExtensions
{
public static IApplicationBuilder UseRequestIP(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestIPMiddleware>();
}
}
}
这样就编写好了一个中间件。
④ 使用中间件。在 Startup.cs 中添加 app.UseRequestIP() 来使用中间件:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
app.UseRequestIP();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});
}
然后运行程序,这里选择使用 Kestrel
访问:http://localhost:5000/

成功运行,到这里我们还可以对这个中间件进行进一步改进,增加更多的功能,如限制访问等。
参考原文
Microsoft 文档 ASP.NET Core 中间件
ASP.NET Core 中间件(Middleware)详解
《ASP.NET Core 跨平台开发从入门到实战》
ASP.NETCore学习记录(二) —— ASP.NET Core 中间件的更多相关文章
- ASP.NETCore学习记录(一)
ASP.NETCore学习记录(一) asp.net core介绍 Startup.cs ConfigureServices Configure 0. ASP.NETCore 介绍 ASP.N ...
- Material Calendar View 学习记录(二)
Material Calendar View 学习记录(二) github link: material-calendarview; 在学习记录一中简单翻译了该开源项目的README.md文档.接下来 ...
- Spring Boot学习记录(二)--thymeleaf模板 - CSDN博客
==他的博客应该不错,没有细看 Spring Boot学习记录(二)--thymeleaf模板 - CSDN博客 http://blog.csdn.net/u012706811/article/det ...
- JavaScript学习记录二
title: JavaScript学习记录二 toc: true date: 2018-09-13 10:14:53 --<JavaScript高级程序设计(第2版)>学习笔记 要多查阅M ...
- 2.VUE前端框架学习记录二
VUE前端框架学习记录二:Vue核心基础2(完结)文字信息没办法描述清楚,主要看编码实战里面,有附带有一个完整可用的Html页面,有需要的同学到脑图里面自取.脑图地址http://naotu.baid ...
- ASP.NET 学习记录之一
(放着期末考试不复习,我每天废寝忘食地阅读从图书馆借来的ASP.NET相关专业书籍,到现在快一个星期,终于掌握了点东西,但是一拿到真刀真枪就做不出来什么了:老师把实验室打开,我就在这码码代码咯) As ...
- Asp.net 学习记录(一)使用asp.net 构建webAPI接口
此系列使用Asp.net构建前后端分离的博客网站. 创建一个asp.net项目 我们这里使用的是空模板,把Https配置去掉(安全先不配置) 构建webapi接口有很多方法,在这里我们选择最简单的2种 ...
- ASP.NET学习记录点滴
1.判读是否是第一次请求,有表单的页面,第一次请求时get请求,而不是post请求,所以可以用来判断请求是否是get,在apsx页面中,有微软封装的属性IsPostBack来判断是否是get还是pos ...
- webrtc学习———记录二:canvas学习
参考资料: http://bucephalus.org/text/CanvasHandbook/CanvasHandbook.html#getcontext2d https://developer.m ...
随机推荐
- 2018.11.07 codeforces559C. Gerald and Giant Chess(dp+组合数学)
传送门 令f[i]f[i]f[i]表示对于第iii个棋子,从(1,1)(1,1)(1,1)出发到它不经过其它棋子的方案数. 于是我们假设(h,w)(h,w)(h,w)有一个棋子,求出它的fff值就可以 ...
- Le Chapitre IV
J'avais ainsi appris une seconde chose très importante: C'est que sa planète d'origine était à peine ...
- hdu6444 2018中国大学生程序设计竞赛 - 网络选拔赛 1007 Neko's loop
Neko's loop Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/65536 K (Java/Others) Total S ...
- php Amome框架 层次设计备注
层次说明: 每一级中函数都是为而且只为 上(高)一层 的文件服务的 最底层: AmemoMySql 基础数据库函数:AmemoConfig 数据库信息配置文件 再高一层: 一个文件对应一个 ...
- jq页面加载分割截图
<script> $(document).ready(function() { if (!Array.prototype.forEach) { Array.prototype.forEac ...
- idea intellij对Spring进行单元测试
1.加入Junit4及SpringJUnit4支持 <!-- junit --> <dependency> <groupId>junit</groupId&g ...
- Typecho 官方文档 接口介绍
官方开发文档实在是太潦草了 Widget_Archive 接口 参数 描述 indexHandle $archive Widget_Archive对象 $select Typecho_Db_Query ...
- python 基础_ 数组的 增删改查3
数组是运用在多个数据存在一个变量中的,而在调用的时候可以调用所需要的数组. 创建数组 a = ['a','b','c','d','f'] #创建一个数组a其中有5个元素分别是abcdf 1.查询.所谓 ...
- (转)eclipse下配置tomcat7的几个重要问题,值得一看
转自:http://jingyan.baidu.com/article/ab69b270ccc4792ca7189fd6.html 这段时间开始接触的servlet,今天尝试在eclipse下配置to ...
- _ZNote_编程语言_Qt_信号槽实现_拖拽方式使用控件
所谓的信号槽,实际上就是观察者模式. 当某个事件发生后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal). 这种发出信号是没有目的的,类似于广播.如果对象对这个信号感兴趣,它就会使 ...