ASP.NET Core的请求处理管道由一个服务器和一组中间件组成,位于 “龙头” 的服务器负责请求的监听、接收、分发和最终的响应,针对请求的处理由后续的中间件来完成。中间件最终体现为一个Func<RequestDelegate, RequestDelegate>委托,但是我们具有不同的定义和注册方式。(本篇提供的实例已经汇总到《ASP.NET Core 6框架揭秘-实例演示版》)

[S1505]以Func<RequestDelegate, RequestDelegate>形式定义中间件(源代码

[S1506]定义强类型中间件类型(源代码

[S1507]定义基于约定的中间件类型(源代码

[S1508]查看默认注册的服务(源代码

[S1509]中间件类型的构造函数注入(源代码

[S1510]中间件类型的方法注入(源代码

[S1511]服务实例的周期(源代码

[S1512]针对服务范围的验证(源代码

[S1505]以Func<RequestDelegate, RequestDelegate>形式定义中间件

如下所示的演示程序创建了两个Func<RequestDelegate, RequestDelegate>委托,它们会在响应中写入两个字符串(“Hello”和“World!”)。在创建出代表承载应用的WebApplication对象之后,我们将其转成IApplicationBuilder接口后(IApplicationBuilder接口的Use方法在WebApplication类型中是显式实现的,所以不得不作这样的类型转换),我们调用其Use方法将这两个委托对象注册为中间件。

var app = WebApplication.Create(args);
IApplicationBuilder applicationBuilder = app;
applicationBuilder
.Use(Middleware1)
.Use(Middleware2);
app.Run(); static RequestDelegate Middleware1(RequestDelegate next) => async context =>
{
await context.Response.WriteAsync("Hello");
await next(context);
};
static RequestDelegate Middleware2(RequestDelegate next) => context => context.Response.WriteAsync(" World!");

运行该程序后,我们利用浏览器对应用监听地址(“http://localhost:5000”)发送请求,两个中间件写入的字符串会以图1所示的形式呈现出来。


图1 利用注册的中间件处理请求

[S1506]定义强类型中间件类型

如果采用强类型中间件类型定义方式,只需要实现如下这个IMiddleware接口。该接口定义了唯一的InvokeAsync方法来处理请求。这个InvokeAsync方法定义了两个参数,前者表示当前HttpContext上下文,后者是一个RequestDelegate委托,代表后续中间件组成的管道。如果当前中间件需要将请求分发给后续中间件进行处理,只需要调用这个委托对象即可,否则针对请求的处理就到此为止。

public interface IMiddleware
{
Task InvokeAsync(HttpContext context, RequestDelegate next);
}

如下所示的演示程序定义了一个实现了IMiddleware接口的StringContentMiddleware中间件类型,实现的InvokeAsync方法将构造函数中指定的字符串作为响应的内容。由于中间件最终是采用依赖注入的方式来提供的,所以需要预先对它注册为服务。用于存放服务注册的 IServiceCollection对象可以通过WebApplicationBuilder的Services属性获得,演示程序利用它完成了针对StringContentMiddleware的服务注册。由于代表承载应用的WebApplication类型实现了IApplicationBuilder接口,所以我们直接调用它的UseMiddleware<TMiddleware>扩展方法来注册中间件类型。启动该程序后利用浏览器访问监听地址,依然可以得到图1所示的输出结果

var builder = WebApplication.CreateBuilder();
builder.Services.AddSingleton<StringContentMiddleware>(new StringContentMiddleware("Hello World!"));
var app = builder.Build();
app.UseMiddleware<StringContentMiddleware>();
app.Run(); public sealed class StringContentMiddleware : IMiddleware
{
private readonly string _contents;
public StringContentMiddleware(string contents)=> _contents = contents;
public Task InvokeAsync(HttpContext context, RequestDelegate next)=> context.Response.WriteAsync(_contents);
}

[S1507]定义基于约定的中间件类型

可能我们已经习惯了通过实现某个接口或者继承某个抽象类的扩展方式,其实这种方式有时显得约束过重,不够灵活,基于约定来定义中间件类型更常用。这种定义方式比较自由,因为它并不需要实现某个预定义的接口或者继承某个基类,而只需要遵循如下这些约定即可

  • 中间件类型需要有一个有效的公共实例构造函数,该构造函数必须包含一个RequestDelegate类型的参数,当中间件实例被创建的时候,代表后续中间件管道的RequestDelegate对象将与这个参数进行绑定。构造函数可以包含任意其他参数,RequestDelegate参数出现的位置也没有限制。
  • 针对请求的处理实现在返回类型为Task的InvokeAsync或者Invoke方法中,它们的第一个参数为HttpContext上下文。约定并未对后续参数作限制,但是由于这些参数最终由依赖注入框架提供,所以相应的服务注册必须存在。

这种方式定义的中间件依然通过前面介绍的UseMiddleware方法和UseMiddleware<TMiddleware>方法进行注册。由于这两个方法会利用依赖注入框架来提供指定类型的中间件对象,所以它会利用注册的服务来提供传入构造函数的参数。如果构造函数的参数没有对应的服务注册,就必须在调用这个方法的时候显式指定。

演示实例定义了如下这个StringContentMiddleware类型,它的InvokeAsync方法会将预先指定的字符串作为响应内容。StringContentMiddleware的构造函数定义了contents和forewardToNext参数,前者表示响应内容,后者表示是否需要将请求分发给后续中间件进行处理。在调用UseMiddleware<TMiddleware>扩展方法对这个中间件进行注册时,我们显式指定了响应的内容,至于参数forewardToNext,我们之所以没有每次都显式指定,是因为默认值的存在。

var app = WebApplication.CreateBuilder().Build();
app
.UseMiddleware<StringContentMiddleware>("Hello")
.UseMiddleware<StringContentMiddleware>(" World!", false);
app.Run(); public sealed class StringContentMiddleware
{
private readonly RequestDelegate _next;
private readonly string _contents;
private readonly bool _forewardToNext; public StringContentMiddleware(RequestDelegate next, string contents, bool forewardToNext = true)
{
_next = next;
_forewardToNext = forewardToNext;
_contents = contents;
} public async Task Invoke(HttpContext context)
{
await context.Response.WriteAsync(_contents);
if (_forewardToNext)
{
await _next(context);
}
}
}

启动该程序后,利用浏览器访问监听地址依然可以得到图1所示的输出结果。对于前面介绍的形式定义的中间件,它们的不同之处除了体现在定义和注册方式上,还体现在自身生命周期上。强类型方式定义的中间件采用的生命周期取决于对应的服务注册,但是按照约定定义的中间件则总是一个单例对象。

[S1508]查看默认注册的服务

ASP.NET Core框架本身在构建请求处理管道之前会注册一些必要的服务,这些公共服务除了供框架自身消费外,也可以供应用程序使用。那么应用启动后究竟预先注册了哪些服务?我们编写了如下这个简单的程序来回答这个问题。

using System.Text;

var builder = WebApplication.CreateBuilder();
var app = builder.Build();
app.Run(InvokeAsync);
app.Run(); Task InvokeAsync(HttpContext httpContext)
{
var sb = new StringBuilder();
foreach (var service in builder.Services)
{
var serviceTypeName = GetName(service.ServiceType);
var implementationType = service.ImplementationType?? service.ImplementationInstance?.GetType()
?? service.ImplementationFactory?.Invoke(httpContext.RequestServices)?.GetType();
if (implementationType != null)
{
sb.AppendLine(@$"{service.Lifetime,-15}{GetName(service.ServiceType),-60}{ GetName(implementationType)}");
}
}
return httpContext.Response.WriteAsync(sb.ToString());
} static string GetName(Type type)
{
if (!type.IsGenericType)
{
return type.Name;
}
var name = type.Name.Split('`')[0];
var args = type.GetGenericArguments().Select(it => it.Name);
return @$"{name}<{string.Join(",", args)}>";
}

演示程序调用WebApplication对象的Run扩展方法注册了一个中间件,它会将每个服务对应的声明类型、实现类型和生命周期作为响应内容进行输出。启动这段程序执行之后,系统注册的所有公共服务会以图2所示的方式输出请求的浏览器上。


图2 ASP.NET Core框架注册的公共服务

[S1509]中间件类型的构造函数注入

在构造函数或者约定的方法中注入依赖服务对象是主要的服务消费方式。对于以处理管道为核心的ASP.NET Core框架来说,依赖注入主要体现在中间件的定义上。由于ASP.NET Core框架在创建中间件对象并利用它们构建整个管道时,所有的服务都已经注册完毕,所以注册的任何一个服务都可以采用如下的方式注入到构造函数中。

using System.Diagnostics;

var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddSingleton<FoobarMiddleware>()
.AddSingleton<Foo>()
.AddSingleton<Bar>();
var app = builder.Build();
app.UseMiddleware<FoobarMiddleware>();
app.Run(); public class FoobarMiddleware : IMiddleware
{
public FoobarMiddleware(Foo foo, Bar bar)
{
Debug.Assert(foo != null);
Debug.Assert(bar != null);
} public Task InvokeAsync(HttpContext context, RequestDelegate next)
{
Debug.Assert(next != null);
return Task.CompletedTask;
}
} public class Foo {}
public class Bar {}

[S1510]中间件类型的方法注入

上面演示的是强类型中间件的定义方式,如果采用约定方式来定义中间件类型,依赖服务还可以采用如下的方式注入用于处理请求的InvokeAsync或者Invoke方法中。

using System.Diagnostics;

var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddSingleton<Foo>()
.AddSingleton<Bar>();
var app = builder.Build();
app.UseMiddleware<FoobarMiddleware>();
app.Run(); public class FoobarMiddleware
{
private readonly RequestDelegate _next;
public FoobarMiddleware(RequestDelegate next) => _next = next;
public Task InvokeAsync(HttpContext context, Foo foo, Bar bar)
{
Debug.Assert(context != null);
Debug.Assert(foo != null);
Debug.Assert(bar != null);
return _next(context);
}
} public class Foo {}
public class Bar {}

[S1511]服务实例的周期

我们演示了如下的实例使读者对注入服务的生命周期具有更加深刻的认识,。如代码片段所示,我们定义了Foo、Bar和Baz三个服务类,它们的基类Base实现了IDisposable接口。我们分别在Base的构造函数和实现的Dispose方法中输出相应的文字,以确定服务实例被创建和释放的时机。

var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Services
.AddSingleton<Foo>()
.AddScoped<Bar>()
.AddTransient<Baz>(); var app = builder.Build();
app.Run(InvokeAsync);
app.Run(); static Task InvokeAsync(HttpContext httpContext)
{
var path = httpContext.Request.Path;
var requestServices = httpContext.RequestServices;
Console.WriteLine($"Receive request to {path}"); requestServices.GetRequiredService<Foo>();
requestServices.GetRequiredService<Bar>();
requestServices.GetRequiredService<Baz>(); requestServices.GetRequiredService<Foo>();
requestServices.GetRequiredService<Bar>();
requestServices.GetRequiredService<Baz>(); if (path == "/stop")
{
requestServices.GetRequiredService<IHostApplicationLifetime>().StopApplication();
}
return httpContext.Response.WriteAsync("OK");
} public class Base : IDisposable
{
public Base() => Console.WriteLine($"{GetType().Name} is created.");
public void Dispose() => Console.WriteLine($"{GetType().Name} is disposed.");
}
public class Foo : Base {}
public class Bar : Base {}
public class Baz : Base {}

我们采用不同的生命周期对这三个服务进行了注册,并将针对请求的处理实现在InvokeAsync这个本地方法中。该方法会从HttpContext上下文中提取出RequestServices,并利用它“两次”提取出三个服务对应的实例。若请求路径为“/stop”,它会采用相同的方式提取出IHostApplicationLifetime对象,并通过调用其StopApplication方法将应用关闭。

我们采用命令行的形式来启动该应用程序,然后利用浏览器依次向该应用发送两个请求,采用的路径分别为 “/index”和“ /stop”,控制台上会出现如图3所示的输出。由于Foo服务采用的生命周期模式为Singleton,所以在整个应用的生命周期内只会创建一次。对于每个接收的请求,虽然Bar和Baz都被使用了两次,但是采用Scoped模式的Bar对象只会被创建一次,而采用Transient模式的Baz对象则被创建了两次。再来看释放服务相关的输出,采用Singleton模式的Foo对象会在应用被关闭的时候被释放,而生命周期模式分别为Scoped和Transient的Bar与Baz对象都会在应用处理完当前请求之后被释放。


图3 服务的生命周期

[S1512]针对服务范围的验证

Scoped服务既不应该由ApplicationServices来提供,也不能注入一个Singleton服务中,否则它将无法在请求结束之后被及时释放。如果忽视了这个问题,就容易造成内存泄漏,下面是一个典型的例子。下面的演示程序使用的FoobarMiddleware的中间件需要从数据库中加载由Foobar类型表示的数据。这里采用Entity Framework Core从SQL Server中提取数据,所以我们为实体类型Foobar定义的DbContext(FoobarDbContext),我们调用IServiceCollection接口的AddDbContext<TDbContext>扩展方法对它以Scoped生命周期进行了注册。

using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations; var builder = WebApplication.CreateBuilder(args);
builder.Host.UseDefaultServiceProvider(options => options.ValidateScopes = false);
builder.Services.AddDbContext<FoobarDbContext>(options => options.UseSqlServer("{your connection string}"));
var app = builder.Build();
app.UseMiddleware<FoobarMiddleware>();
app.Run(); public class FoobarMiddleware
{
private readonly RequestDelegate _next;
private readonly Foobar? _foobar;
public FoobarMiddleware(RequestDelegate next, FoobarDbContext dbContext)
{
_next = next;
_foobar = dbContext.Foobar.SingleOrDefault();
} public Task InvokeAsync(HttpContext context)
{
return _next(context);
}
} public class Foobar
{
[Key]
public string Foo { get; set; }
public string Bar { get; set; }
} public class FoobarDbContext : DbContext
{
public DbSet<Foobar> Foobar { get; set; }
public FoobarDbContext(DbContextOptions options) : base(options) { }
}

采用约定方式定义的中间件实际上是一个单例对象,而且它是在应用启动时中由ApplicationServices创建的。由于FoobarMiddleware的构造函数中注入了FoobarDbContext对象,所以该对象自然也成了一个单例对象,这就意味着FoobarDbContext对象的生命周期会延续到当前应用程序被关闭的那一刻,造成的后果就是数据库连接不能及时地被释放。

using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations; var builder = WebApplication.CreateBuilder(args);
builder.Host.UseDefaultServiceProvider(options => options.ValidateScopes = true);
builder.Services.AddDbContext<FoobarDbContext>(options => options.UseSqlServer("{your connection string}"));
var app = builder.Build();
app.UseMiddleware<FoobarMiddleware>();
app.Run();
...

在一个ASP.NET Core应用中,如果将服务的生命周期注册为Scoped模式,我们希望服务实例真正采用基于请求的生命周期模式。我们可以通过启用针对服务范围的验证来避免采用作为根容器的IServiceProvider对象来提供Scoped服务实例。针对服务范围的检验开关可以调用IHostBuilder接口的UseDefaultServiceProvider扩展方法进行设置。如果我们采用上面的方式开启针对服务范围验证,启动该程序之后会出现图4所示的异常。由于此验证会影响性能,所以默认情况下此开关只有在“Development”环境下才会被开启。


图4 针对Scoped服务的验证

ASP.NET Core 6框架揭秘实例演示[24]:中间件的多种定义方式的更多相关文章

  1. ASP.NET Core 6框架揭秘实例演示[07]:文件系统

    ASP.NET Core应用具有很多读取文件的场景,如读取配置文件.静态Web资源文件(如CSS.JavaScript和图片文件等).MVC应用的视图文件,以及直接编译到程序集中的内嵌资源文件.这些文 ...

  2. ASP.NET Core 6框架揭秘实例演示[08]:配置的基本编程模式

    .NET的配置支持多样化的数据源,我们可以采用内存的变量.环境变量.命令行参数.以及各种格式的配置文件作为配置的数据来源.在对配置系统进行系统介绍之前,我们通过几个简单的实例演示一下如何将具有不同来源 ...

  3. ASP.NET Core 6框架揭秘实例演示[09]:配置绑定

    我们倾向于将IConfiguration对象转换成一个具体的对象,以面向对象的方式来使用配置,我们将这个转换过程称为配置绑定.除了将配置树叶子节点配置节的绑定为某种标量对象外,我们还可以直接将一个配置 ...

  4. ASP.NET Core 6框架揭秘实例演示[10]:Options基本编程模式

    依赖注入使我们可以将依赖的功能定义成服务,最终以一种松耦合的形式注入消费该功能的组件或者服务中.除了可以采用依赖注入的形式消费承载某种功能的服务,还可以采用相同的方式消费承载配置数据的Options对 ...

  5. ASP.NET Core 6框架揭秘实例演示[11]:诊断跟踪的几种基本编程方式

    在整个软件开发维护生命周期内,最难的不是如何将软件系统开发出来,而是在系统上线之后及时解决遇到的问题.一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根 ...

  6. ASP.NET Core 6框架揭秘实例演示[12]:诊断跟踪的进阶用法

    一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根据当前的运行状态预知未来可能发生的问题,并将问题扼杀在摇篮中.诊断跟踪能够帮助我们有效地纠错和排错&l ...

  7. ASP.NET Core 6框架揭秘实例演示[13]:日志的基本编程模式[上篇]

    <诊断跟踪的几种基本编程方式>介绍了四种常用的诊断日志框架.其实除了微软提供的这些日志框架,还有很多第三方日志框架可供我们选择,比如Log4Net.NLog和Serilog 等.虽然这些框 ...

  8. ASP.NET Core 6框架揭秘实例演示[14]:日志的进阶用法

    为了对各种日志框架进行整合,微软创建了一个用来提供统一的日志编程模式的日志框架.<日志的基本编程模式>以实例演示的方式介绍了日志的基本编程模式,现在我们来补充几种"进阶" ...

  9. ASP.NET Core 6框架揭秘实例演示[15]:针对控制台的日志输出

    针对控制台的ILogger实现类型为ConsoleLogger,对应的ILoggerProvider实现类型为ConsoleLoggerProvider,这两个类型都定义在 NuGet包"M ...

随机推荐

  1. Junit4进行参数化测试

    @RunWith, 当类被@RunWith注解修饰,或者类继承了一个被该注解修饰的类,JUnit将会使用这个注解所指明的运行器(runner)来运行测试,而不是JUnit默认的运行器. 要进行参数化测 ...

  2. 框架3.2--搭建V·P·N

    目录 部署OpenVPN 一.服务端 1.安装openvpn和证书工具 2.生成服务器配置文件 3.准备证书签发相关文件 4.准备签发证书相关变量的配置文件 5.初始化PKI生成PKI相关目录和文件 ...

  3. Redis 源码简洁剖析 12 - 一条命令的处理过程

    命令的处理过程 Redis server 和一个客户端建立连接后,会在事件驱动框架中注册可读事件--客户端的命令请求.命令处理对应 4 个阶段: 命令读取:对应 readQueryFromClient ...

  4. suse 12 二进制部署 Kubernetets 1.19.7 - 第12章 - 部署dashboard插件

    文章目录 1.12.0.创建namespace 1.12.1.创建Dashboard rbac文件 1.12.2.创建dashboard文件 1.12.3.查看pod以及svc 1.12.4.获取 d ...

  5. 深入MySQL(二):MySQL的数据类型

    前言 对于MySQL中的数据类型的选择,不同的数据类型看起来可能是相同的效果,但是其实很多时候天差地别. 本章从MySQL中的常用类型出发,结合类型选择的常见错误,贯彻MySQL的常用类型选择. 常用 ...

  6. jenkins针对不同用户显示不同项目

    网上看了别人写的博客有点头晕 比如:https://www.cnblogs.com/kazihuo/p/9022899.html  典型的权限混乱,te用户可以读re用户的项目,re用户可以读te用户 ...

  7. 微服务从代码到k8s部署应有尽有系列(七、支付服务)

    我们用一个系列来讲解从需求到上线.从代码到k8s部署.从日志到监控等各个方面的微服务完整实践. 整个项目使用了go-zero开发的微服务,基本包含了go-zero以及相关go-zero作者开发的一些中 ...

  8. Go Exec 僵尸与孤儿进程

    原文地址:Go Exec 僵尸与孤儿进程 最近,使用 golang 去管理本地应用的生命周期,期间有几个有趣的点,今天就一起看下. 场景一 我们来看看下面两个脚本会产生什么问题: 创建两个 shell ...

  9. 开源爱好者月刊《HelloGitHub》第 71 期

    兴趣是最好的老师,HelloGitHub 让你对编程感兴趣! 简介 HelloGitHub 分享 GitHub 上有趣.入门级的开源项目. https://github.com/521xueweiha ...

  10. 什么是jQuery?

    目录 一:jQuery 1.jQuery介绍 2.jQuery的宗旨 3.有了jQuery那我们还使用BOM与DOM吗? 4.jQuery的优势 5.python与jQuery导入(复习) 6.jQu ...