解读ASP.NET 5 & MVC6系列(6):Middleware详解
在第1章项目结构分析中,我们提到Startup.cs
作为整个程序的入口点,等同于传统的Global.asax
文件,即:用于初始化系统级的信息(例如,MVC中的路由配置)。本章我们就来一一分析,在这里如何初始化这些系统级的信息。
新旧版本之间的Pipeline区别
ASP.NET 5和之前版本的最大区别是对HTTP Pipeline的全新重写,在之前的版本中,请求过滤器的通常是以HttpModule
为模块组件,这些组件针对HttpApplication
里定义的各个周期内的事件进行响应,从而用于实现认证、全局错误处理、日志等功能。传统的Form表单认证就是一个HTTPModule
。HTTPModule
不仅能够过滤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 =>{ /*...*/});
示例中的UseStaticFiles
、UseIdentity
、UseMvc
都是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
中就包括了我们常操作的HttpRequest
和HttpResponse
实例对象。
注意:
HttpContext
、HttpRequest
、HttpResponse
在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
同步与推荐
本文已同步至目录索引:解读ASP.NET 5 & MVC6系列
解读ASP.NET 5 & MVC6系列(6):Middleware详解的更多相关文章
- 解读ASP.NET 5 & MVC6系列(11):Routing路由
新版Routing功能介绍 在ASP.NET 5和MVC6中,Routing功能被全部重写了,虽然用法有些类似,但和之前的Routing原理完全不太一样了,该Routing框架不仅可以支持MVC和We ...
- 解读ASP.NET 5 & MVC6系列(8):Session与Caching
在之前的版本中,Session存在于System.Web中,新版ASP.NET 5中由于不在依赖于System.Web.dll库了,所以相应的,Session也就成了ASP.NET 5中一个可配置的模 ...
- 解读ASP.NET 5 & MVC6系列
本系列的大部分内容来自于微软源码的阅读和网络,大部分测试代码都是基于VS RC版本进行测试的. 解读ASP.NET 5 & MVC6系列(1):ASP.NET 5简介 解读ASP.NET 5 ...
- 解读ASP.NET 5 & MVC6系列(2):初识项目
初识项目 打开VS2015,创建Web项目,选择ASP.NET Web Application,在弹出的窗口里选择ASP.NET 5 Website模板创建项目,图示如下: 我们可以看到,此时Web ...
- 解读ASP.NET 5 & MVC6系列(7):依赖注入
在前面的章节(Middleware章节)中,我们提到了依赖注入功能(Dependency Injection),ASP.NET 5正式将依赖注入进行了全功能的实现,以便开发人员能够开发更具弹性的组件程 ...
- [转]解读ASP.NET 5 & MVC6系列(7):依赖注入
本文转自:http://www.cnblogs.com/TomXu/p/4496440.html 在前面的章节(Middleware章节)中,我们提到了依赖注入功能(Dependency Inject ...
- 解读ASP.NET 5 & MVC6 ---- 系列文章
本系列的大部分内容来自于微软源码的阅读和网络,大部分测试代码都是基于VS RC版本进行测试的. 解读ASP.NET 5 & MVC6系列(1):ASP.NET 5简介 解读ASP.NET 5 ...
- [转]解读ASP.NET 5 & MVC6系列(8):Session与Caching
本文转自:http://www.cnblogs.com/TomXu/p/4496445.html 在之前的版本中,Session存在于System.Web中,新版ASP.NET 5中由于不在依赖于Sy ...
- 解读ASP.NET 5 & MVC6系列(17):MVC中的其他新特性
(GlobalImport全局导入功能) 默认新建立的MVC程序中,在Views目录下,新增加了一个_GlobalImport.cshtml文件和_ViewStart.cshtml平级,该文件的功能类 ...
随机推荐
- 用U盘安卓esxi虚拟机出现 error loading /s.v00 错误解决办法
前段时间用 一个 白色的 东芝U盘 给戴尔 R720服务器安装 esxi 6.0时,在加载到/s.v00 找个文件时出现错误,大致为: error loading /s.v00 compressed ...
- yum 源
epel 6源: cd /usr/local/src wget https://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noar ...
- 浅谈WEB前后端分离
重审业务逻辑 用过MVC的童鞋都知道业务逻辑(Bussiness Logic),但是大多对这概念又是模棱两可,业务逻辑从来都是这样难以理解,谈论前后端分离之前这个概念非常有必要探讨一下! 在简单的CR ...
- Nginx - Linux下按天分割日志
待完善. 可参考:https://www.iteblog.com/archives/1244
- VS2012 还原默认设置
恢复默认设置的2种方法 如果VS出现问题或设置变乱,可以通过恢复默认设置使之回到安装成功时的状态,从而解决出现的问题.VS恢复默认设置的方法有2种,分别是:通过"导入和导出设置"实 ...
- ehcache注解全面解析---打酱油的日子
通过ehcache以编程方式使用缓存: 跟上面的方式相同,但是缓存通过ehcache去管理,当然比使用map有N多种好处,比如缓存太大了快达到上限之后,将哪一部分缓存清除出去.这种方式完全是通过代码的 ...
- 【Linux】Linux统计文件夹、文件数量的命令
# 查看当前目录下的文件数量(不包含子目录中的文件) ls -l|grep "^-"| wc -l # 查看当前目录下的文件数量(包含子目录中的文件) 注意:R,代表子目录 ls ...
- 【原】iOS学习之tableView的常见BUG
1.TableView头视图不随视图移动,头视图出现错位 错误原因:tableView的 UITableViewStyle 没有明确的声明 解决方法:在tableView声明的时候明确为 UITabl ...
- HDU-SupportOrNot训练实录
菜鸡队训练实录. 现场赛记录: 2016:[名称:奖项/排名] ZJPSC:Gold/1 CCPC中南邀请赛:Gold/1 ICPC Dalian:Gold/24 ICPC Beijing:Gold/ ...
- sublime插件
CnDict: 中英文字典软件,快捷键查词,目前支持金山词霸和有道词典. BracketHighlighter: 有个笑话,说前苏联间谍花了巨大的代价,偷到了阿波罗飞船的最后一屏的代码,发现全部是 } ...