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 中间件的更多相关文章

  1. ASP.NETCore学习记录(一)

    ASP.NETCore学习记录(一) asp.net core介绍  Startup.cs  ConfigureServices  Configure  0. ASP.NETCore 介绍 ASP.N ...

  2. Material Calendar View 学习记录(二)

    Material Calendar View 学习记录(二) github link: material-calendarview; 在学习记录一中简单翻译了该开源项目的README.md文档.接下来 ...

  3. Spring Boot学习记录(二)--thymeleaf模板 - CSDN博客

    ==他的博客应该不错,没有细看 Spring Boot学习记录(二)--thymeleaf模板 - CSDN博客 http://blog.csdn.net/u012706811/article/det ...

  4. JavaScript学习记录二

    title: JavaScript学习记录二 toc: true date: 2018-09-13 10:14:53 --<JavaScript高级程序设计(第2版)>学习笔记 要多查阅M ...

  5. 2.VUE前端框架学习记录二

    VUE前端框架学习记录二:Vue核心基础2(完结)文字信息没办法描述清楚,主要看编码实战里面,有附带有一个完整可用的Html页面,有需要的同学到脑图里面自取.脑图地址http://naotu.baid ...

  6. ASP.NET 学习记录之一

    (放着期末考试不复习,我每天废寝忘食地阅读从图书馆借来的ASP.NET相关专业书籍,到现在快一个星期,终于掌握了点东西,但是一拿到真刀真枪就做不出来什么了:老师把实验室打开,我就在这码码代码咯) As ...

  7. Asp.net 学习记录(一)使用asp.net 构建webAPI接口

    此系列使用Asp.net构建前后端分离的博客网站. 创建一个asp.net项目 我们这里使用的是空模板,把Https配置去掉(安全先不配置) 构建webapi接口有很多方法,在这里我们选择最简单的2种 ...

  8. ASP.NET学习记录点滴

    1.判读是否是第一次请求,有表单的页面,第一次请求时get请求,而不是post请求,所以可以用来判断请求是否是get,在apsx页面中,有微软封装的属性IsPostBack来判断是否是get还是pos ...

  9. webrtc学习———记录二:canvas学习

    参考资料: http://bucephalus.org/text/CanvasHandbook/CanvasHandbook.html#getcontext2d https://developer.m ...

随机推荐

  1. 软件推荐-国内参数优化软件:1stOpt - First Optimizationg

    首页:http://www.7d-soft.com/index.htm 4.0新功能 (预定2010年8月6日): 1:支持复数拟合.复数方程组计算: 2:支持微分方程拟合求解: 3:通用全局优化求解 ...

  2. 第12章:MongoDB-CRUD操作--文档--查询--游标详解

    ①是什么游标 游标不是查询结果,可以理解为数据在遍历过程中的内部指针,其返回的是一个资源,或者说数据读取接口. 客户端通过对游标进行一些设置就能对查询结果进行有效地控制,如可以限制查询得到的结果数量. ...

  3. ubuntu 出现device not managed,解决方法

    1. 编辑/etc/NetworkManager/NetworkManager.conf: sudo vi /etc/NetworkManager/NetworkManager.conf将其中的man ...

  4. Typecho 调用分类文章列表

    其中pageSize后面的数字表示调用文章的数量:mid后面的数字表示调用的分类ID; 提示:Typecho分类目录ID的获取方法是把鼠标移到某分类名称上面,在浏览器状态栏显示的mid=后面的数字便是 ...

  5. Path类对路径字符串的操作

    在写程序时,不时会用到处理文件路径的问题,例如:取得扩展名.从路径中取出文件名.路径合并.取出或者去年扩展名等.这些功能都可以通过System.IO.Path类提供的方法来实现.这些相关功能用过多次了 ...

  6. 合理提升WEB前端性能

    前端的优化包括四个部分:HTML结构优化.CSS样式优化.JS行为优化.服务器的优化.合理的前端优化不仅能够提升网站加载速度,而且能够更好的提升用户体验和团队开发效率.所以前端性能优化的重要性是不言而 ...

  7. 【repost】 原生JS执行环境与作用域深入理解

    首先,我们要知道执行环境和作用域是两个完全不同的概念. 函数的每次调用都有与之紧密相关的作用域和执行环境.从根本上来说,作用域是基于函数的,而执行环境是基于对象的(例如:全局执行环境即window对象 ...

  8. java基础-day14

    第01天 java面向对象 今日内容介绍 u 接口 u 匿名对象&final u 多态 第1章   接口 1.1  接口的概述 接口是功能的集合,同样可看做是一种数据类型,是比抽象类更为抽象的 ...

  9. Spring的两种代理JDK和CGLIB的区别浅谈

    一.原理区别: java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理. 而cglib动态代理是利用asm开源包,对代理对象类的class文件 ...

  10. FileZilla_server在Windows和Linnx下的部署安装

    1. FileZilla官网下载FileZilla Server服务器,目前最新版本为0.9.53. 2. 安装FileZilla服务器.除以下声明的地方外,其它均采用默认模式,如安装路径等. 2.1 ...