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. js 数组map用法 Array.prototype.map()

    map 这里的map不是"地图"的意思,而是指"映射".[].map(); 基本用法跟forEach方法类似: array.map(callback,[ thi ...

  2. js正则表达式 (.+)与(.+?)

    (.+)默认是贪婪匹配 (.+?)为惰性匹配 疑问号让.+的搜索模式从贪婪模式变成惰性模式. var str = 'aaa<div style="font-color:red;&quo ...

  3. 精简的言语讲述技术人,必须掌握基础性IT知识技能,第一篇

    前言 此系列将以精简的言语讲述技术人,必须掌握基础性IT知识技能,请持续关注,希望给大家都是一些精简的干货. 第一部分:必须掌握的设计模式的6大基本原则 23个设计模式,都是从这六大设计模式中演化而来 ...

  4. Python基础—文件操作(Day8)

    一.文件操作参数 1.文件路径 1)绝对路径:从根目录开始一级一级查找直到找到文件. f=open('e:\文件操作笔记.txt',encoding='utf-8',mode='r') content ...

  5. Dubbo源码剖析六之SPI扩展点的实现之getExtension

    上文Dubbo源码剖析六之SPI扩展点的实现之getExtensionLoader - 池塘里洗澡的鸭子 - 博客园 (cnblogs.com)中分析了getExtensionLoader,本文继续分 ...

  6. 树莓派GPIO开发(一):激光头传感器模块的使用

    配置环境 系统:Raspbian11(64位) 设备:树莓派CM4 一.写在前面 主要为了测试我捡漏买的CM4的拓展版 拓展板子没有焊接引脚,但是预留的接口 手动焊接一下 测试成功 ,说明我捡的这块板 ...

  7. swagger 2.0

    1.引入jar包 <dependency> <groupId>io.springfox</groupId> <artifactId>springfox- ...

  8. mysql 去除前后空白字符

    update  table  set  field = replace(replace(replace(field,char(9),''),char(10),''),char(13),'');

  9. 如何在3D场景中在模型上面绘制摄取点

    有些时候,我们在屏幕上面绘制一个摄取点,在单屏玩游戏的模式下,我们并不能觉得有什么不妥.但是最近VR的热火朝天,我们带上眼镜看双屏的时候,总觉得这个摄取点看着很不舒服. 这个问题该怎么解决?在这里我首 ...

  10. 【C#基础概念】常量

    常量的定义 常量是在编译时设置其值并且永远不能更改其值的字段. 使用常量可以为特殊值提供有意义的名称,而不是数字文本 常量是不可变的值,在编译时是已知的,在程序的生命周期内不会改变. 常量使用 con ...