前言
其实地上本没有路,走的人多了,也便成了路。 -- 鲁迅
就像上面鲁迅说的那样,其实在我们开发中间件的过程中,微软并没有制定一些策略或者文档来约束你如何编写一个中间件程序, 但是其中却存在者一些最佳实践的方法,大多数人来使用这种方法来使应用程序变得更加容易理解并且易于维护,这就叫“路”,在2017年,这叫套路。
在掌握了这些套路之后,能够帮助你迅速的搭建一个中间件的基本框架,并且易于扩展和维护,下面我们就来看看怎么样从头开始开发一个中间件吧。
如果你对 ASP.NET Core HTTP 管道还不太清楚的话,下面这篇文章将有助于你对其进行一个系统的了解:
说明: 这只是通常情况下,具体的情况还请使用具体的套路。
Setup 1 创建扩展类如果你的中间件需要一个向 ASP.NET Core 的 DI 容器中添加默认的一些服务的话,那么你就编写一个需要扩展类,用来在 Startup.cs 中的 ConfigureServices 中注册服务。
举例,Microsoft.AspNetCore.ResponseCompression 这是一个用来压缩请求内容的一个中间件,那么它就需要一个服务用来处理压缩相关的东西,所以它扩展了 IServiceCollection 并且添加了自己的 Services。
整个中间件的核心代码并非在这里,这里只是一个开始,那么有同学可能会问了,什么情况下我们需要提前向一个DI里面注入我们中间件需要的服务呢? 答案是,如果你不知道或者不确定你需要什么样的服务的时候,跳过此步骤,进入下一步,再等你需要的时候再回头来补上就是。
那么,我们先看一下编写一个扩展Service的静态类应该怎么做?
首先,新建一个以 xxxServicesExtensions 文件名结尾的静态类,用来编写注入DI的扩展方法。
类建立完成之后,需要向里面添加内容了。通常情况下,中间件中 Service 的扩展方法都是以 Addxxx(this IServiceCollection services) 开头来命名。在这里有一个需要注意的地方就是它的命名空间,通常情况下我们使用 using Microsoft.AspNetCore.Builder 这个命名空间。
然后,方法里面就是需要注册的服务了。假设我们需要向里面注册一个 IResponseCompressionProvider 和 它的实现类 ResponseCompressionProvider,那么最终的扩展方法可能看起来是这样的。
using System; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; namespace Microsoft.AspNetCore.Builder { public static IServiceCollection AddResponseCompression(this IServiceCollection services) { if (services == null) { throw new ArgumentNullException(nameof(services)); } services.TryAddSingleton<IResponseCompressionProvider, ResponseCompressionProvider>(); return services; } } Setup 2 创建配置类有的时候,用户在使用我们编写的中间件的时候,我们需要向提供者提供一些配置项,这些配置项在中间件执行之前用来传递一些必要参数信息或者是一些设置信息。举个我们熟悉例子,我们在使用 MVC 中间件的时候,可能会看到以下写法:
// Startup.cs public void ConfigureServices(IServiceCollection services) { var userDefinedFilter = new xxxFilter(); services.AddMvc(x => x.Filters.Add(userDefinedFilter)); }可以看到,用户可将一些自定义的 Filter 传入到中间件的,然后中间件在运行的时候,我们传入的 Filter 就生效了。
注意,中间件使用的配置项有两种添加方法,一种是添加到 AddMiddleware(Action<xxxOptions> option) 另外一种是 UseMiddleware<>(Action<xxxOptions> option),那么这两种有什么区别呢?
那么,前者Add中的配置项一般情况下是中间执行之前就需要的一些信息,也就是说中间件的启动就依赖于这些配置项,他放置于容器配置(Add DI Service)的时候添加进去更加方便或者合适的时候使用它,另外一种(后者)是容器已经构建完毕,不需要依赖于容器提供的配置项可以使用此种方式。
同样的道理,当你自己为你的用户编写一个中间件的时候,当你也需要用户可以自定义一些配置或者需要传入一些参数的时候,你也可以这么做。那到底怎么样做呢? 我们一起来看看。
首先,我们需要一个 xxxOptions 结尾的配置类,用来配置我们中间件需要的一些配置项。我们还是以上面的压缩中间件举例。
public class GzipCompressionProviderOptions : IOptions<GzipCompressionProviderOptions> { public CompressionLevel Level { get; set; } = CompressionLevel.Fastest; GzipCompressionProviderOptions IOptions<GzipCompressionProviderOptions>.Value => this; }它其中配置了一个压缩的等级 CompressionLevel ,这是一个枚举字段。 然后我们可以看到,这个类它继承了 IOptions<out T> 接口,这是一个知识点,什么意思呢? IOptions<out TOptions> 是 ASP.NET Core 新的一个配置体系里面的一个接口,当你实现这个接口之后,ASP.NET Core DI 容器提供了了一个 services.Configure<xxxOptions> 这样的方法来让你把配置项注入到容器中,当然你也可以将配置项和 appsetting.json 中的配置关联起来,以便于配置一些在运行期可能需要变动信息。更多关于 IOptions<T> 的信息可以看 。
这个 xxxOptions 类通常情况下会提供一些默认值,也就是说当用户不提供这些参数的时候,你需要有一个合理的机制或者默认值来正常运行你的中间件。
假如你的配置项有很多,也就是说还有进一步比较细化的配置,那么你可以做一个封装,就像MVC的Options类一样,这样能够给你的中间件提供更加合理的维护和扩展。
Setup 3 核心中间件接下来,就是我们的核心代码类了,通常情况下会有一个 xxxMiddleware 结尾的类用来处理 HTTP 管道请求中的一些业务,这个类的构造函数中已经可以使用在Setup1或者Setup2中向DI容器中注册的服务了。
按照约定,Middleware 类中需要有一个 Invoke 方法,用来处理中间件的核心业务,它的签名如下:
public Task Invoke(HttpContext httpContext);注意,这是一个约定方法,并没有接口来约束它。在 Invoke 方法中,是中间件实现的核心代码。 示例如下:
public class xxxMiddleware { private readonly RequestDelegate _next; public xxxMiddleware(RequestDelegate next) { if (next == null) { throw new ArgumentNullException(nameof(next)); } _next = next; } public async Task Invoke(HttpContext context) { // ...... await _next(context); return; } }在 xxxMiddleware 这个里面有一个构造函数参数 RequestDelegate,它是一个委托,代表的需要执行的下一个中间件,通常情况下我们会把它放到我们业务代码的末尾。
Setup 4 中间件扩展注册中间件有三种注册方法(Run,Map,Use),我们暂不考虑Run和Map,因为他们只适用于很小和少的一些情况