《ASP.NET Core 6框架揭秘》实例演示[24]:中间件的多种定义方式

ASP.NET Core的请求处理管道由一个服务器和一组中间件组成,位于 “龙头” 的服务器负责请求的监听、接收、分发和最终的响应,针对请求的处理由后续的中间件来完成。中间件最终体现为一个Func<RequestDelegate, RequestDelegate>委托,但是我们具有不同的定义和注册方式。[本文节选《ASP.NET Core 6框架揭秘》第15章]

[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所示的形式呈现出来。

6b323c00ab91a59228cae81e7d147a78.png
图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所示的方式输出请求的浏览器上。

98a72f234f18b8cdd6fb845b519daa69.png
图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对象都会在应用处理完当前请求之后被释放。

a3767c6f518ad5565bb92e5937f5ce7c.png
图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”环境下才会被开启。

cd484e11e5dd0753b4ee912f049fc8e0.png
图4 针对Scoped服务的验证

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/282901.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Android之 RecyclerView,CardView 详解和相对应的上拉刷新下拉加载

为什么80%的码农都做不了架构师&#xff1f;>>> 随着 Google 推出了全新的设计语言 Material Design&#xff0c;还迎来了新的 Android 支持库 v7&#xff0c;其中就包含了 Material Design 设计语言中关于 Card 卡片概念的实现 —— CardView。RecyclerView也是谷…

Java——Arrays类操作数组的工具类

JDK中提供了一个专门用于操作数组的工具类&#xff0c;即 Arrays 类&#xff0c;位于 Java。util 包中。该类提供了一系列方法来操作数组&#xff0c;如排序、复制、比较、填充等&#xff0c;用户直接调用这些方法即可&#xff0c;不需要自己编码实现&#xff0c;降低了开发难度…

CORS——跨域请求那些事儿

【本期嘉宾介绍】睿得&#xff0c;具有多年研发、运维、安全等IT相关从业经历。目前从事CDN、存储、视频直播点播的技术支持。喜爱钻研&#xff0c;喜爱编码&#xff0c;喜爱分享。 在日常的项目开发时会不可避免的需要进行跨域操作&#xff0c;而在实际进行跨域请求时&#xf…

oracle 数据执行计划,Oracle里常见的执行计划

本文介绍了Oracle数据库里常见的执行计划&#xff0c;使用的Oracle数据库版本为11.2.0.1。1、与表访问相关的执行计划Oracle数据库里与表访问有关的两种方法&#xff1a;全表扫描和ROWID扫描。反映在执行计划上&#xff0c;与全表扫描对应的执行计划中的关键字是“TABLE ACCESS…

.NET MAUI实战 Dispatcher

详细内容这一期分享的内容非常简单&#xff0c;在之前使用过WPF的开发者对MVVM开发模式下ViewModel中后台线程转UI线程并不陌生使用Appplication.Current.Dispatcher。那么在.NET MAUI中也有同样的机制&#xff0c;存在于.NET MAUI Shell对象中。那么什么是Shell&#xff1f;官…

GDB 配置

GDB 配置 使用 GDB 扩展来配置 GDB 事实上我还是觉得原生的 GDB 就挺好&#xff0c;速度快&#xff0c;需要查看什么执行命令就可以。 GDB DashBoard https://github.com/cyrus-and/gdb-dashboard $sudo mkdir -m 777 ~/gdbinit; cd ~/gdbinit $git clone https://github.com/c…

Oracle区分中文和英文,oracle中中英文段落划分实现

oracle中关于中文占用字节数&#xff0c;不同的数据库有不同的情况&#xff0c;有的占用两个字节、有的占用三个字节&#xff0c;现在测试环境的数据库中文占用三个字节&#xff0c;要实现由中英文组成的段落字符串&#xff0c;按照每行占用多少字节重新分段&#xff0c;具体应…

虚拟机网络配置详解(NAT、桥接、Hostonly)

VirtualBox中有四种网络连接方式: NATBridged AdapterInternalHost-only AdapterVMWare中有三种&#xff0c;其实它跟VMWare的网络连接方式都是一样的概念&#xff0c;只是比VMWare多了Internal方式 在介绍四种工作模式之前&#xff0c;先说下虚拟网卡&#xff0c;虚拟机安装好…

微软宣布正式开源 Azure IoT Edge 边缘计算服务

开发四年只会写业务代码&#xff0c;分布式高并发都不会还做程序员&#xff1f; 微软宣布&#xff0c;去年年底公开预览的 Azure IoT Edge 边缘计算服务已进入官方版&#xff0c;并通过 GitHub 将其开源。Azure IoT Edge 主要将基于云的分析和定制的业务逻辑转移到边缘设备&a…

Windows下安装BeautifulSoup

电脑首先要安装好了python&#xff0c;我安装的是2.7。 下面就是bs4的安装过程了: 1.去官网下载BeautifulSoup4 2017.02.10目前最新版本&#xff1a;Beautiful Soup 4.3.2 2.解压文件 将下载得到的压缩包解压到任意文件夹&#xff0c;路径不含中文 3.打开cmd命令提示符 winr&am…

BZOJ1578: [Usaco2009 Feb]Stock Market 股票市场

S<50只股票D<10天的价格给出&#xff0c;求第一天开始用n<200000元最后能得到的最大钱数&#xff0c;保证答案<500000。 做D次完全背包即可&#xff0c;每次做完把dp数组清空。 1 #include<cstdio>2 #include<cstring>3 #include<algorithm>4 #i…

OC如何跳到系统设置里的各种设置界面

当 iOS系统版本 < iOS7时 , 只能跳转到 系统设置页面 &#xff0c;楼主试了下&#xff0c;非真机是没有任何效果的 当iOS系统版本 < iOS 10.0 时 NSURL *url [NSURL URLWithString:"prefs:rootLOCATION_SERVICES"]; if( [[UIApplication sharedApplication]can…

虚拟DOM Diff算法解析

React中最神奇的部分莫过于虚拟DOM&#xff0c;以及其高效的Diff算法。这让我们可以无需担心性能问题而”毫无顾忌”的随时“刷新”整个页面&#xff0c;由虚拟DOM来确保只对界面上真正变化的部分进行实际的DOM操作。React在这一部分已经做到足够透明&#xff0c;在实际开发中我…

Azure 跨订阅迁移资源踩坑记

突然收到微软的邮件&#xff0c;提示我的一个 Azure 订阅已经到期&#xff0c;所以转为“禁用”状态&#xff0c;只能进行数据的导出和处理。在这个订阅里有不少较重要的资源在跑&#xff0c;直接关了可不行…于是开启了一个支持事件&#xff0c;台湾美眉的态度和声线真的没话说…

《ASP.NET Core 6框架揭秘》实例演示[25]:配置与承载环境的应用

与服务注册一样&#xff0c;针对配置的设置同样可以采用三种不同的编程模式。第一种是利用WebApplicationBuilder的Host属性返回的IHostBuilder对象&#xff0c;它可以帮助我们设置面向宿主和应用的配置。IWebHostBuilder接口上面同样提供了一系列用来对配置进行设置的方法&…

Linux日志出现大量kernel: NET: Registered protocol family 36

一台Linux服务器的系统错误日志出现大量的“ kernel: NET: Registered protocol family 36”错误信息&#xff0c;如下所示&#xff1a; Jul 2 05:27:45 xxxxxx kernel: NET: Registered protocol family 36Jul 2 05:27:45 xxxxxx kernel: NET: Unregistered protocol family…

node的模块机制

Node.js模块的实现 之前在网上查阅了许多介绍Node.js的文章&#xff0c;可惜对于Node.js的模块机制大都着墨不多。在后续介绍模块的使用之前&#xff0c;我认为有必要深入一下Node.js的模块机制。 CommonJS规范 早在Netscape诞生不久后&#xff0c;JavaScript就一直在探索本地编…

httpstat:一个检查网站性能的 curl 统计分析工具

httpstat&#xff1a;一个检查网站性能的 curl 统计分析工具httpstat 是一个 Python 脚本&#xff0c;它以美妙妥善的方式反映了 curl 统计分析&#xff0c;它是一个单一脚本&#xff0c;兼容 Python 3 &#xff0c;在用户的系统上不需要安装额外的软件(依赖)。作者&#xff1a…

关于面试中看到一些问题

最近公司在招聘.NET开发人员&#xff0c;面试了一些人&#xff0c;有一些感悟&#xff0c;分享出来&#xff0c;以供参考。面试的人员中&#xff0c;有一些是三五年的开发人员&#xff1b;也有几个是10年左右的技术负责人&#xff0c;不但自己架构过项目&#xff0c;还有带领导…

一个countDown在多线程调度下使用不当的分享

2019独角兽企业重金招聘Python工程师标准>>> 一个countDown在多线程调度下使用不当的分享 1. 诡异的数据抖动 在一个需求开发过程中&#xff0c;由于有多角色需要获取每个角色下的菜单&#xff1b;结果出现了单角色下拉去菜单没问题&#xff0c;多角色情况下只有一个…