解读ASP.NET 5 & MVC6系列教程(6):Middleware详解

内容摘要
在第1章项目结构分析中,我们提到Startup.cs作为整个程序的入口点,等同于传统的Global.asax文件,即:用于初始化系统级的信息(例如,MVC中的路由配置)。本章我们就来一一分析,在这里如
文章正文

在第1章项目结构分析中,我们提到Startup.cs作为整个程序的入口点,等同于传统的Global.asax文件,即:用于初始化系统级的信息(例如,MVC中的路由配置)。本章我们就来一一分析,在这里如何初始化这些系统级的信息。

新旧版本之间的Pipeline区别

ASP.NET 5和之前版本的最大区别是对HTTP Pipeline的全新重写,在之前的版本中,请求过滤器的通常是以HttpModule为模块组件,这些组件针对HttpApplication里定义的各个周期内的事件进行响应,从而用于实现认证、全局错误处理、日志等功能。传统的Form表单认证就是一个HTTPModuleHTTPModule不仅能够过滤Request请求,还可以和Response响应进行交互并修改。这些HTTPModule组件都继承于IHttpModule接口,而该接口是位于System.Web.dll中。

HttpModule代码不仅可以在Global.asax中的各事件周期中进行添加,还可以单独编译成类库并在web.config中进行注册。

新版的ASP.NET 5抛弃了重量级的System.Web.dll,相应地引入了Middleware的概念,Middleware的官方定义如下:

Pass through components that form a pipeline between a server and application to inspect, route, or modify request and response messages for a specific purpose.
在服务器和应用程序之间的管线Pipeline之间,针对特定的目的,穿插多个Middleware组件,从而对request请求和response响应进行检
查、路由、或修改。

该定义和传统的HttpModule以及HttpHandler特别像。

Middleware的注册和配置

在ASP.NET5中,request请求管线(Pipeline)的访问是在Startup类中进行的,该类时一个约定类,并且里面的ConfigureServices方法、Configure方法、以及相应的参数也是事先约定的,所以不能进行改动。

Middleware中的依赖处理:ConfigureServices方法

在ASP.NET5中的各种默认的Middleware中,都使用了依赖注入的功能,所以在使用Middleware中的功能时,需要提前将依赖注入所需要的类型及映射关系都注册到依赖注入管理系统中,即IServiceCollection集合,而ConfigureServices方法接收的就一个IServiceCollection类型的参数,该参数就是所有注册过类型的集合,通过原生的依赖注入组件进行管理(关于ASP.NET5中的依赖注入,我们会在单独章节中进行讲解),在该方法内,我们可以向该集合中添加新的类型和类型映射关系,示例如下:

// Add MVC services to the services container.
services.AddMvc();

示例中的代码用于向系统添加Mvc模块相关的Service类型以支撑MVC功能,该方法是一个扩展方法,用于在集合中添加与MVC相关的多个类型。

Middleware的注册和配置:Configure方法

Configure方法的签名如下:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerfactory)
{
 // ...
}

Configure方法接收了三个参数:IApplicationBuilder类型的参数用于构建整个应用程序的配置信息,IHostingEnvironment类的env参数用于访问系统环境变量相关的内容,ILoggerFactory类型的loggerfactory用于日志相关的内容处理,其中IApplicationBuilder类型的参数最为重要,该参数实例app上有一系列的扩展方法用于将各种Middleware注册到request请求管线(Pipeline)中。这种方式和之前ASP.NET中的HTTP管线的主要区别是:新版本中的组合模型替换了旧版本中的事件模型。这也就要求,在新版ASP.NET中,Middleware组件注册的顺序是非常重要的,因为后一个组件可能要使用到前一个组件,所以必须按照依赖的先后顺序进行注册,举例如下,当前MVC项目的模板代码示例如下:

// Add static files to the request pipeline.
app.UseStaticFiles();

// Add cookie-based authentication to the request pipeline.
app.UseIdentity();

// Add MVC to the request pipeline.
app.UseMvc(routes =>{ /*...*/});

示例中的UseStaticFilesUseIdentityUseMvc都是IApplicationBuilder上的扩展方法,在扩展方法中,都会通过调用扩展方法app.UseMiddleware方法,最终再调用app.Use方法来注册新的Middleware,该方法定义如下:

public interface IApplicationBuilder
{
 //...
 IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
}

通过代码,可以看出,middleware是Func<RequestDelegate, RequestDelegate>的一个实例,该Func接收一个RequestDelegate的参数,并返回一个RequestDelegate类型的值。RequestDelegate的源码如下:

public delegate Task RequestDelegate(HttpContext context);

通过源码,我们可以看出,RequestDelegate是一个委托函数,其接收HttpContext类型的实例,并返回一个Task类型的异步对象。也就是说RequestDelegate是一个可以返回自身RequestDelegate类型函数的函数,整个ASP.NET也就是利用这种方式构建了管线(Pipelien)的组成,在这里,每个middleware都链式到下一个middleware上,并在整个过程中可以对HttpConext对象进行修改或维护,当然,HttpContext中就包括了我们常操作的HttpRequestHttpResponse实例对象。

注意:HttpContextHttpRequestHttpResponse在ASP.NET 5中都是重新定义的新类型。

Middleware的定义

既然每个middleare都是Func<RequestDelegate, RequestDelegate>的一个实例,那是不是Middleware的定义要满足一个规则?是继承于一个抽象基类还是借口?通过翻查相关的代码,我们看到,Middleware是基于约定的形式来定义的,具体约定规则如下:

构造函数的第一个参数必须是处理管线中的下一个处理函数,即RequestDelegate;必须有一个 Invoke 函数, 并接受上下文参数(即HttpContent), 然后返回 Task;

示例如下:

public class MiddlewareName
{
 RequestDelegate _next;

 public MiddlewareName(RequestDelegate next)
 {
  _next = next;// 接收传入的RequestDelegate实例
 }

 public async Task Invoke(HttpContext context)
 {
  // 处理代码,如处理context.Request中的内容

  Console.WriteLine("Middleware开始处理");

  await _next(context);

  Console.WriteLine("Middleware结束处理");

  // 处理代码,如处理context.Response中的内容
 }
}

通过该模板代码可以看到,首先一个Middleware的构造函数要接收一个RequestDelegate的实例,先保存在一个私有变量里,然后通过调用Invoke方法(并接收HttpContent实例)并返回一个Task,并且在调用Invoke的方法中,要通过await _next(context);语句,链式到下一个Middleware上,我们的处理代码主要就是在链式语句的前后执行相关的代码。

举个例子,如果我们要想记录页面的执行时间,首先,我们先定义一个TimeRecorderMiddleware,代码如下:

public class TimeRecorderMiddleware
{
 RequestDelegate _next;

 public TimeRecorderMiddleware(RequestDelegate next)
 {
  _next = next;
 }

 public async Task Invoke(HttpContext context)
 {
  var sw = new Stopwatch();
  sw.Start();


  await _next(context);

  var newDiv = @"<div id=""process"">页面处理时间:{0} 毫秒</div></body>";
  var text = string.Format(newDiv, sw.ElapsedMilliseconds);
  await context.Response.WriteAsync(text);
 }
}

Middleware的注册有很多种方式,如下是实例型注册代码:

app.Use(next => new TimeRecorderMiddleware(next).Invoke);

或者,你也可以使用UseMiddleware扩展方法进行注册,示例如下:

app.UseMiddleware<TimeRecorderMiddleware>();
//app.UseMiddleware(typeof(TimeRecorderMiddleware)); 两种方式都可以

当然,你也可以定义一个自己的扩展方法用于注册该Middleware,代码如下:

public static IApplicationBuilder UseTimeRecorderMiddleware(this IApplicationBuilder app)
{
 return app.UseMiddleware<TimeRecorderMiddleware>();
}

最后在Startup类的Configure方法内进行注册,代码如下:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerfactory)
{
 app.UseTimeRecorderMiddleware(); // 要放在前面,以便进行统计,如果放在Mvc后面的话,就统计不了时间了。

 // 等等
}

编译,重启,并访问页面,在页面的底部即可看到页面的运行时间提示内容。

常用Middleware功能的使用

app.UseErrorPage()
在IHostingEnvironment.EnvironmentName为Development的情况下,才显示错误信息,并且错误信息的显示种类,可以通过额外的ErrorPageOptions参数来设定,可以设置全部显示,也可以设置只显示Cookies、Environment、ExceptionDetails、Headers、Query、SourceCode SourceCodeLineCount中的一种或多种。

app.UseErrorHandler("/Home/Error")
捕获所有的程序异常错误,并将请求跳转至指定的页面,以达到友好提示的目的。

app.UseStaticFiles()
开启静态文件也能走该Pipeline管线处理流程的功能。

app.UseIdentity()
开启以cookie为基础的ASP.NET identity认证功能,以支持Pipeline请求处理。

直接使用委托定义Middleware的功能

由于Middleware是Func<RequestDelegate, RequestDelegate>委托类型的实例,所以我们也可以不必定义一个单独的类,在Startup类里,使用委托调用的方式就可以了,示例如下:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerfactory)
{
 app.Use(new Func<RequestDelegate, RequestDelegate>(next => content => Invoke(next, content)));
 // 其它
}

// 注意Invoke方法的参数
private async Task Invoke(RequestDelegate next, HttpContext content)
{
 Console.WriteLine("初始化组件开始");
 await next.Invoke(content);
 Console.WriteLine("管道下步执行完毕");
}

做个简便的Middleware基类

虽然有约定方法,但有时候我们在开发的时候往往会犯迷糊,想不起来到底是什么样的约定,所以,在这里我们可以定义一个抽象基类,然后以后所有的Middleware在定义的时候都继承该抽象类并重载Invoke方法即可,从而可以避免约定忘记的问题。代码如下:

/// <summary>
/// 抽象基类
/// </summary>
public abstract class AbstractMiddleware
{
 protected RequestDelegate Next { get; set; }
 protected AbstractMiddleware(RequestDelegate next)
 {
  this.Next = next;
 }
 public abstract Task Invoke(HttpContext context);
}

/// <summary>
/// 示例Middleware
/// </summary>
public class DemoMiddleware : AbstractMiddleware
{
 public DemoMiddleware(RequestDelegate next) : base(next)
 {
 }
 public async override Task Invoke(HttpContext context)
 {
  Console.WriteLine("DemoMiddleware Start.");
  await Next.Invoke(context);
  Console.WriteLine("DemoMiddleware End.");
 }
}

使用方法和上面的一样。

终止链式调用或阻止所有的Middleware

在有些情况下,当然根据某些条件判断以后,可能不在需要继续往下执行下去了,而是想知己诶返回结果,那么你可以在你的Middleware里忽略对await next.Invoke(content);的调用,直接使用·Response.WriteAsync·方法输出内容。

另外,在有些情况下,你可能需要实现类似之前版本中的handler的功能,即不经常任何Pipeline直接对Response进行响应,新版ASP.NET里提供了一个run方法用于实现该功能,只需要在Configure方法里调用如下代码即可实现类似的内容输出。

app.Run(async context =>
{
 context.Response.ContentType = "text/html";
 await context.Response.WriteAsync("Hello World!");
});

关于ASP.NET 5 Runtime的内容,请访问:https://msdn.microsoft.com/en-us/magazine/dn913182.aspx

遗留问题

在Mvc项目中,所有的依赖注入类型都是通过IServiceProvider实例来获取的,目前可以通过以下形式获取该实例:

var services = Context.RequestServices; // Controller中
var services = app.ApplicationServices; // Startup中

获取了该实例以后,即可通过如下方法来获取某个类型的对象:

var controller = (AccountController)services.GetService(typeof(AccountController));
// 要判断获取到的对象是否为null

如果你引用了Microsoft.Framework.DependencyInjection命名空间的话,还可以使用如下三种扩展方法:

var controller2 = (AccountController)services.GetService<AccountController>(); 
// 要判断获取到的对象是否为null

//如下两种方式,如果获取到的AccountController实例为null的话,就会字段抛异常,而不是返回null
var controller3 = (AccountController)services.GetRequiredService(typeof(AccountController));
var controller4 = (AccountController)services.GetRequiredService<AccountController>();

那么问题来了?如何不在Startup和Controller里就可以获取到HttpContext和IApplicationBuilder实例以便使用这些依赖注入服务?

如何获取IApplicationBuilder实例?
答案:在Startup里将IApplicationBuilder实例保存在一个单例中的变量上,后期全站就可以使用了。

如何获取HttpContext实例?
答案:参考依赖注入章节的普通类的依赖注入

引用:http://www.mikesdotnetting.com/article/269/asp-net-5-middleware-or-where-has-my-httpmodule-gone


代码注释

作者:喵哥笔记

IDC笔记

学的不仅是技术,更是梦想!