什么是中间件

中间件是一种装配到应用管道,以处理请求和响应的组件。每个中间件:

  • 选择是否将请求传递到管道中的下一个中间件。
  • 可在管道中的下一个中间件前后执行。

ASP.NET Core请求管道包含一系列请求委托,依次调用。工作原理:

PS:类似于ASP.NET里的Handler(处理程序)和Module(模块)。

HTTP模块和处理程序的工作原理:

创建中间件管道

Run委托不会收到next参数。第一个Run委托始终为终端,用于终止管道。Run是一种约定。

public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World!");
});
}
}

Use将多个请求委托链接在一起。next参数表示管道中的下一个委托。可通过不调用next参数使用管道短路。通常可在上一个委托前后执行操作:

public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
// Do work that doesn't write to the Response.
await next.Invoke();
// Do logging or other work that doesn't write to the Response.
}); app.Run(async context =>
{
await context.Response.WriteAsync("Hello from 2nd delegate.");
});
}
}

警告:在向客户端发送Response后,不要再调用next.Invoke。Response启动后,针对HttpResponse的修改会引起异常。调用next后写入Response正文:

  • 可能导致违反协议。例如,写入的长度超过规定的Content-Length。
  • 可能损坏正文格式。例如,向CSS文件中写入HTML页脚。

HasStarted是一个有用的提示,指示是否已发送标头或已写入正文。

中间件顺序

中间件是通过请求管道逐一执行的,通过Startup.Configure方法里添加中间件的顺序来定义了请求管道的顺序。

如上图所示,自定义的中间件放在Authorization中间件和Endpoint之间。

以下是ASP.NET Core项目默认配置的中间件顺序:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
} app.UseHttpsRedirection();
app.UseStaticFiles();
// app.UseCookiePolicy(); app.UseRouting();
// app.UseRequestLocalization();
// app.UseCors(); app.UseAuthentication();
app.UseAuthorization();
// app.UseSession(); app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}

中间件分支

当我们需要对某些请求进行特定处理时,可以通过创建管道分支来实现。Map扩展方法用于创建管道分支。

public class Startup
{
private static void HandleMapTest1(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 1");
});
} private static void HandleMapTest2(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 2");
});
} public void Configure(IApplicationBuilder app)
{
app.Map("/map1", HandleMapTest1); app.Map("/map2", HandleMapTest2); app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
});
}
}

MapWhen是用于对特定谓词的结果来创建请求管道分支。相比于Map(只能对针对特定的Url)更加高级。

public class Startup
{
private static void HandleBranch(IApplicationBuilder app)
{
app.Run(async context =>
{
var branchVer = context.Request.Query["branch"];
await context.Response.WriteAsync($"Branch used = {branchVer}");
});
} public void Configure(IApplicationBuilder app)
{
app.MapWhen(context => context.Request.Query.ContainsKey("branch"),
HandleBranch); app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
});
}
}

UseWhen也是用于对特定谓词的结果来创建请求管道分支。但与MapWhen不同的是,如果这个分支不发生短路或包含终端中间件,则会重新加入主管道。

public class Startup
{
private readonly ILogger<Startup> _logger; public Startup(ILogger<Startup> logger)
{
_logger = logger;
} private void HandleBranchAndRejoin(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
var branchVer = context.Request.Query["branch"];
_logger.LogInformation("Branch used = {branchVer}", branchVer); // Do work that doesn't write to the Response.
await next();
// Do other work that doesn't write to the Response.
});
} public void Configure(IApplicationBuilder app)
{
app.UseWhen(context => context.Request.Query.ContainsKey("branch"),
HandleBranchAndRejoin); app.Run(async context =>
{
await context.Response.WriteAsync("Hello from main pipeline.");
});
}
}

自定义中间件

下面以多语言版本功能为例,介绍如何编写自定义中间件。

多语言版本功能指根据所在区域,以所在区域的语言显示网站内容。例如在中国以汉字显示、在美国以英文显示、在俄罗斯以俄文显示。

下面代码是直接在Startup.Configure编写,通过参数culture手动指定语言版本,例如请求https://localhost:5001/?culture=zh-cn

public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery); CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
} // Call the next delegate/middleware in the pipeline
await next();
}); app.Run(async (context) =>
{
await context.Response.WriteAsync(
$"Hello {CultureInfo.CurrentCulture.DisplayName}");
}); }
}

一般来说,编写自定义中间件使用类,既符合单一职责原则,也更方便管理。

using Microsoft.AspNetCore.Http;
using System.Globalization;
using System.Threading.Tasks; namespace Culture
{
public class RequestCultureMiddleware
{
private readonly RequestDelegate _next; public RequestCultureMiddleware(RequestDelegate next)
{
_next = next;
} public async Task InvokeAsync(HttpContext context)
{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery); CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture; } // Call the next delegate/middleware in the pipeline
await _next(context);
}
}
}

中间件类必须满足:

  • RequestDelegate类型参数的公共构造函数。
  • 有名字为InvokeInvokeAsync的方法,并且方法要满足:
    • 返回值是Task
    • 方法参数第一个是HttpContext类型。

在实际开发中,中间件会用到其他服务,由于中间件是在应用启动时创建的,生命周期是Singleton的,不与其他依赖注入的服务共享生命周期。如果需要使用依赖注入,可以通过InvokeInvokeAsync方法注入。

public class CustomMiddleware
{
private readonly RequestDelegate _next; public CustomMiddleware(RequestDelegate next)
{
_next = next;
} // IMyScopedService is injected into Invoke
public async Task Invoke(HttpContext httpContext, IMyScopedService svc)
{
svc.MyProperty = 1000;
await _next(httpContext);
}
}

最后,我们习惯性在Startup.Configure中,通过扩展方法来注册中间件。

using Microsoft.AspNetCore.Builder;

namespace Culture
{
public static class RequestCultureMiddlewareExtensions
{
public static IApplicationBuilder UseRequestCulture(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestCultureMiddleware>();
}
}
} public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.UseRequestCulture(); app.Run(async (context) =>
{
await context.Response.WriteAsync(
$"Hello {CultureInfo.CurrentCulture.DisplayName}");
});
}
}

参考资料

[ASP.NET Core开发实战]基础篇03 中间件的更多相关文章

  1. [ASP.NET Core开发实战]基础篇02 依赖注入

    ASP.NET Core的底层机制之一是依赖注入(DI)设计模式,因此要好好掌握依赖注入的用法. 什么是依赖注入 我们看一下下面的例子: public class MyDependency { pub ...

  2. [ASP.NET Core开发实战]基础篇01 Startup

    Startup,顾名思义,就是启动类,用于配置ASP.NET Core应用的服务和请求管道. Startup有两个主要作用: 通过ConfigureServices方法配置应用的服务.服务是一个提供应 ...

  3. [ASP.NET Core开发实战]基础篇06 配置

    配置,是应用程序很重要的组成部分,常常用于提供信息,像第三方应用登录钥匙.上传格式与大小限制等等. ASP.NET Core提供一系列配置提供程序读取配置文件或配置项信息. ASP.NET Core项 ...

  4. [ASP.NET Core开发实战]基础篇05 服务器

    什么是服务器 服务器指ASP.NET Core应用运行在操作系统上的载体,也叫Web服务器. Web服务器实现侦听HTTP请求,并以构建HttpContext的对象发送给ASP.NET Core应用. ...

  5. [ASP.NET Core开发实战]基础篇04 主机

    主机定义 主机是封闭应用资源的对象. 设置主机 主机通常由 Program 类中的代码配置.生成和运行. HTTP项目(ASP.NET Core项目)创建泛型主机: public class Prog ...

  6. 2月送书福利:ASP.NET Core开发实战

    大家都知道我有一个公众号“恰童鞋骚年”,在公众号2020年第一天发布的推文<2020年,请让我重新介绍我自己>中,我曾说到我会在2020年中每个月为所有关注“恰童鞋骚年”公众号的童鞋们送一 ...

  7. [ASP.NET Core开发实战]开篇词

    前言 本系列课程文章主要是学习官方文档,再输出自己学习心得,希望对你有所帮助. 课程大纲 本系列课程主要分为三个部分:基础篇.实战篇和部署篇. 希望通过本系列课程,能让大家初步掌握使用ASP.NET ...

  8. ASP.NET Core 2.2 基础知识(二) 中间件

    中间件是一种装配到应用管道以处理请求和相应的软件.每个软件都可以: 1.选择是否将请求传递到管道中的下一个组件; 2.可在调用管道中的下一个组件前后执行工作. 管道由 IApplicationBuil ...

  9. 《ASP.NET Core项目开发实战入门》带你走进ASP.NET Core开发

    <ASP.NET Core项目开发实战入门>从基础到实际项目开发部署带你走进ASP.NET Core开发. ASP.NET Core项目开发实战入门是基于ASP.NET Core 3.1 ...

随机推荐

  1. [转]Post和Get的区别

    作者:zhanglinblog     来源:https://urlify.cn/FnYBbu 这个问题几乎面试的时候都会问到,是一个老生常谈的话题,然而随着不断的学习,对于以前的认识有很多误区,所以 ...

  2. maven中的陌生单词

    有个单词记不住啊: artifact:人工制品,手工艺品,加工品; 石器; 词根:fac,fact,fect,fic,fig=make,do,表示“做,制作”   因此 art i fact 意思很好 ...

  3. 基于AspectCore打造属性注入

    前言 源自于晓晨在成都.net社区群的一篇文章 <晓晨的ASP.NET Core 奇淫技巧之伪属性注入> 他的思路是 Ioc容器替换 ControllerActivator,因为只能在控制 ...

  4. .Net Core下基于Emit的打造AOP

    之前的基于DispatchProxy的AOP组件,实现了属性注入,但是这个依旧有很多限制 比如不支持构造器注入,继承DispatchProxy的子类必须是公开类 个人有点代码洁癖,不喜欢这种不能控制的 ...

  5. 电力规约DL/T 654 2007多功能电表通信协议 调试工具

    DL/T 654 2007多功能电表通信协议 调试工具 最近调试DL/T654 2007电力规约,在网上找到一个比较好用的工具,分享给大家,希望对大家有帮助. CSDN需要积分,我传百度网盘了, 内含 ...

  6. 002_centos7关闭防火墙

    防火墙是比较烦人的,在自己做实验,或者实际应用中,如果配置不好的话,会出现各种匪夷所思的问题,那么如何关闭呢 在centos7里,防火墙改为了firewalld进程 首先用命令firewall-cmd ...

  7. Java实现获取一个随机的两位数

    import java.util.Random; //获取一个随机的 两位数public class getrandomdouble { public static void main(String[ ...

  8. linux常用命令(一)软件操作命令

    软件包管理器:yum 安装软件:yum install xxx 卸载软件:yum remove xxx 搜索软件:yum search xxx 清理缓存:yum clean packages 列出已安 ...

  9. Homekit_Dohome_智能插座

    简介: 本款智能插座有三个版本可供选择,分别为Homekit版本,涂鸦版本,Dohome版本,各个版本的区别如下: DoHome版特点: 支持HomeKit 支持Amazon 支持Google ...

  10. 《JavaScript语言入门教程》记录整理:面向对象

    目录 面向对象编程 实例对象与 new 命令 this关键字 对象的继承 Object对象的方法 严格模式(strict mode) 本系列基于阮一峰老师的<JavaScrip语言入门教程> ...