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

From:汤姆大叔的博客 http://www.cnblogs.com/TomXu/p/4496435.html

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

已转载:http://www.cnblogs.com/imust2008/p/4917771.html

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

新旧版本之间的Pipeline区别

ASP.NET 5和之前版本的最大区别是对HTTP Pipeline的全新重写,(参见:http://docs.asp.net/en/latest/conceptual-overview/aspnet.html#redesign

With ASP.NET 5 we are making a number of architectural changes that make the core web framework much leaner and more modular. ASP.NET 5 is no longer based on System.Web.dll, but is instead based on a set of granular and well factored NuGet packages allowing you to optimize your app to have just what you need. You can reduce the surface area of your application to improve security, reduce your servicing burden and also to improve performance in a true pay-for-what-you-use model.)

在之前的版本中,请求过滤器的通常是以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是基于约定的形式来定义的,具体约定规则如下:

  1. 构造函数的第一个参数必须是处理管线中的下一个处理函数,即RequestDelegate;
  2. 必须有一个 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实例以便使用这些依赖注入服务?

    1. 如何获取IApplicationBuilder实例?
      答案:在Startup里将IApplicationBuilder实例保存在一个单例中的变量上,后期全站就可以使用了。
    2. 如何获取HttpContext实例?
      答案:参考依赖注入章节的普通类的依赖注入
时间: 2024-10-13 19:27:21

解读ASP.NET 5 & MVC6系列(6):Middleware详解的相关文章

解读ASP.NET 5 &amp; MVC6 ---- 系列文章

本系列的大部分内容来自于微软源码的阅读和网络,大部分测试代码都是基于VS RC版本进行测试的. 解读ASP.NET 5 & MVC6系列(1):ASP.NET 5简介 解读ASP.NET 5 & MVC6系列(2):初识项目 解读ASP.NET 5 & MVC6系列(3):项目发布与部署 解读ASP.NET 5 & MVC6系列(4):核心技术与环境配置 解读ASP.NET 5 & MVC6系列(5):Configuration配置信息管理 解读ASP.NET 5

[转]解读ASP.NET 5 &amp; MVC6系列(7):依赖注入

本文转自:http://www.cnblogs.com/TomXu/p/4496440.html 在前面的章节(Middleware章节)中,我们提到了依赖注入功能(Dependency Injection),ASP.NET 5正式将依赖注入进行了全功能的实现,以便开发人员能够开发更具弹性的组件程序,MVC6也利用了依赖注入的功能重新对Controller和View的服务注入功能进行了重新设计:未来的依赖注入功能还可能提供更多的API,所有如果还没有开始接触依赖注入的话,就得好好学一下了. 在之

解读ASP.NET 5 &amp; MVC6系列(7):依赖注入

解读ASP.NET 5 & MVC6系列(7):依赖注入 2015-05-20 09:10 by 汤姆大叔, 9986 阅读, 26 评论, 收藏, 编辑 在前面的章节(Middleware章节)中,我们提到了依赖注入功能(Dependency Injection),ASP.NET 5正式将依赖注入进行了全功能的实现,以便开发人员能够开发更具弹性的组件程序,MVC6也利用了依赖注入的功能重新对Controller和View的服务注入功能进行了重新设计:未来的依赖注入功能还可能提供更多的API,所

解读ASP.NET 5 &amp; MVC6系列

解读ASP.NET 5 & MVC6系列 2015-05-13 07:37 by 汤姆大叔, 22761 阅读, 33 评论, 收藏, 编辑 本系列的大部分内容来自于微软源码的阅读和网络,大部分测试代码都是基于VS RC版本进行测试的. 解读ASP.NET 5 & MVC6系列(1):ASP.NET 5简介 解读ASP.NET 5 & MVC6系列(2):初识项目 解读ASP.NET 5 & MVC6系列(3):项目发布与部署 解读ASP.NET 5 & MVC6系

解读ASP.NET 5 &amp; MVC6系列(2):初识项目

原文:解读ASP.NET 5 & MVC6系列(2):初识项目 初识项目 打开VS2015,创建Web项目,选择ASP.NET Web Application,在弹出的窗口里选择ASP.NET 5 Website模板创建项目,图示如下: 我们可以看到,此时Web Forms\MVC\Web API复选框都选择不了,原有是因为在ASP.NET 5中做了大量更改,移除了Web Forms功能,将MVC.Web API.Web Pages这些功能合在了一起,所以自然就不需要这些复选框了.另外由于是CT

解读ASP.NET 5 &amp; MVC6系列(8):Session与Caching

原文:解读ASP.NET 5 & MVC6系列(8):Session与Caching 在之前的版本中,Session存在于System.Web中,新版ASP.NET 5中由于不在依赖于System.Web.dll库了,所以相应的,Session也就成了ASP.NET 5中一个可配置的模块(middleware)了. 配置启用Session ASP.NET 5中的Session模块存在于Microsoft.AspNet.Session类库中,要启用Session,首先需要在project.json

解读ASP.NET 5 &amp; MVC6系列(5):Configuration配置信息管理

解读ASP.NET 5 & MVC6系列(5):Configuration配置信息管理 2015-05-18 07:44 by 汤姆大叔, 7103 阅读, 18 评论, 收藏, 编辑 在前面的章节中,我们知道新版的MVC程序抛弃了原来的web.config文件机制,取而代替的是config.json,今天我们就来深入研究一下配置文件的相关内容. 基本用法 新版的配置信息机制在Microsoft.Framework.ConfigurationModel命名空间下进行了重写,重写以后不仅支持XML

解读ASP.NET 5 &amp; MVC6系列(17):MVC中的其他新特性

原文:解读ASP.NET 5 & MVC6系列(17):MVC中的其他新特性 (GlobalImport全局导入功能) 默认新建立的MVC程序中,在Views目录下,新增加了一个_GlobalImport.cshtml文件和_ViewStart.cshtml平级,该文件的功能类似于之前Views目录下的web.config文件,之前我们在该文件中经常设置全局导入的命名空间,以避免在每个view文件中重复使用@using xx.xx语句.默认的示例如下: @using BookStore @usi

解读ASP.NET 5 &amp; MVC6系列(14):View Component

原文:解读ASP.NET 5 & MVC6系列(14):View Component 在之前的MVC中,我们经常需要类似一种小部件的功能,通常我们都是使用Partial View来实现,因为MVC中没有类似Web Forms中的WebControl的功能.但在MVC6中,这一功能得到了极大的改善.新版MVC6中,提供了一种叫做View Component的功能. 你可以将View Component看做是一个mini的Controller--它只负责渲染一小部分内容,而非全部响应,所有Parti

解读ASP.NET 5 &amp; MVC6系列(15):MvcOptions配置

原文:解读ASP.NET 5 & MVC6系列(15):MvcOptions配置 程序模型处理 IApplicationModelConvention 在MvcOptions的实例对象上,有一个ApplicationModelConventions属性(类型是:List<IApplicationModelConvention>),该属性IApplicationModelConvention类型的接口集合,用于处理应用模型ApplicationModel,该集合是在MVC程序启动的时候进