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. 2019.01.21 bzoj2441: [中山市选2011]小W的问题(树状数组+权值线段树)

    传送门 数据结构优化计数菜题. 题意简述:给nnn个点问有多少个www型. www型的定义: 由5个不同的点组成,满足x1<x2<x3<x4<x5,x3>x1>x2 ...

  2. jQuery 常用效果

    hide和show 同样有 fadeInhe fadeOut 的功能 $(document).ready(function(){ $("#hide").click(function ...

  3. AttributeError: type object 'testClass' has no attribute 'testMothod'

    点击"Unittest for test_post_API.testClass"按钮,点击”Edit configuration...“,弹出对话框Run/Debug config ...

  4. Everything的简单使用

    1.Everythings下载地址: http://www.voidtools.com/ 下载完后直接解压,运行everything.exe即可打开使用: 2.基本设置 (1)去除不需要搜索的文件夹: ...

  5. 第20章:MongoDB-聚合操作--聚合管道--$unwind

    ①$unwind 在查询数据的时候经常会返回数组信息,但是数组并不方便信息的浏览,所以提供有“$unwind”可以将数组数据变为独立的字符串内容. 将文档中数组类型的字段拆分成多条,每条文档包含数组中 ...

  6. Python 错误和异常小结[转]

    原文链接    http://blog.csdn.net/sinchb/article/details/8392827 事先说明哦,这不是一篇关于Python异常的全面介绍的文章,这只是在学习Pyth ...

  7. Matlab作图

    修改线宽 plot(x,y,'LineWidth',1.5) 在xlabel,ylabel,title中使用latex语言 xlabel('$x$','interpreter','latex') yl ...

  8. (转)EntityFramework.Extensions

    转自:http://www.symbolsource.org/Public/Metadata/NuGet/Project/EntityFramework.Extended/1.0.0.20/Relea ...

  9. (01背包 先排序)Proud Merchants (hdu 3466)

    http://acm.hdu.edu.cn/showproblem.php?pid=3466   Description Recently, iSea went to an ancient count ...

  10. Codeforces Round #298 (Div. 2)--D. Handshakes

    #include <stdio.h> #include <algorithm> #include <set> using namespace std; #defin ...