应用程序对HTTP请求的处理过程进行划分,每个环节称为中间件,将各个中间件串联起来,就形成了HTTP管道。
执行中间件的顺序与它们添加到管道的顺序相同,先添加的中间件会先执行。
添加方法有三种:
(1)委托:中间件专用委托类型为RequestDelegate
,对应的方法结构就是带HttpContext
类型的输入参数,并返回Task
对象。一般委托方式适用于代码量较少,处理逻辑比较简单的中间件。
(2)基于约定的中间件类:基于约定的中间件类必须包含Invoke
或InvokeAsync
方法,输入参数为一个HttpContext
对象,并返回Task
对象。中间件类可以通过构造函数的依赖注入来获取下一个中间件的方法引用。
(3)实现IMiddleware接口:该接口同样包含Invoke
或InvokeAsync
方法,输入参数为一个HttpContext
对象,并返回Task
对象。用这种定义的中间件需要在代码中显式将其添加到服务容器中,因此此种中间件可修改生命周期,而前两种都是单例,应用程序中只创建一次实例。
以委托形式定义中间件
- 下面的例子创建了三个中间件,并在每个中间件代码中生成了一个字符串,在最后一个中间件中拼接并打印。
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.Use(async (context, next) =>
{
context.Items["line1"] = "第一环节,完成";
await next();
})
.Use(async (context, next) =>
{
context.Items["line2"] = "第二环节,完成";
await next();
})
.Use(async (context, next) =>
{
context.Items["line3"] = "第三环节,完成";
// 拼接回应消息
var parts = (from o in context.Items select o.Value.ToString()).AsEnumerable();
string responseMessage = string.Join("<br/>", parts);
// 设置编码
context.Response.ContentType = "text/html;charset=UTF-8";
await context.Response.WriteAsync(responseMessage);
await Task.CompletedTask;
});
}
定义中间件类
通过约定方式定义中间件类。一般会戴上“Middleware”作为后缀。必须包含Invoke
或InvokeAsync
方法。除了HttpContext类型参数外,可在该方法后面定义其他参数,支持依赖注入。
public class SampleMiddleware
{
// 下一个中间件的引用
private readonly RequestDelegate m_next;
public SampleMiddleware(RequestDelegate next)
{
m_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
await context.Response.WriteAsync("Hello Web");
// 调用下一个中间件
await m_next(context);
}
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMiddleware<SampleMiddleware>();
}
带参数的中间件
中间件允许使用参数,但并不是调用参数,而且在中间件注册时使用,即生命周期内参数只传递一次。通过构造函数传递,第一个参数是HTTP管道下一个中间件引用,第二个参数开始可以定义中间件参数。
public class CalcuMiddleware
{
readonly RequestDelegate _next;
readonly int _a, _b;
public CalcuMiddleware(RequestDelegate next, int a, int b)
{
_next = next;
_a = a;
_b = b;
}
public async Task InvokeAsync(HttpContext context)
{
int result = _a * _b;
context.Response.ContentType = "text/html;charset=UTF-8";
await context.Response.WriteAsync($"{_a}×{_b}={result}");
await _next(context);
}
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMiddleware<CalcuMiddleware>(15, 7);
}
IMiddleware接口的用途
基于约定的中间件类在程序运行期间只创建单例,而实现了IMiddleware
接口的中间件类的生命周期就可以灵活控制。IMiddleware接口实现的中间件,在Startup.Configure
方法中调用UseMiddleware
方法之前,必须在Startup.ConfigureServices
方法中进行注册。
public class TestMiddleware : IMiddleware
{
public TestMiddleware()
{
Console.WriteLine($"类 {GetType().Name} 的构造函数被调用");
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// 添加两个响应头
context.Response.Headers["item 1"] = "hello";
context.Response.Headers["item 2"] = "hi";
// 写入响应消息
context.Response.ContentType = "text/html;charset=UTF-8";
await context.Response.WriteAsync("欢迎来到主页");
}
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<TestMiddleware>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMiddleware<TestMiddleware>();
}
}
让HTTP管道“短路”
直接调用IApplicationBuilder
的Run
扩展方法,会使整个HTTP请求管道发生“短路”,即直接把响应消息发回给客户端,终止此次HTTP通信。
- 如下面代码
app.Run(async context =>
{
await context.Response.WriteAsync("你好,世界");
});
app.Run(async context =>
{
await context.Response.WriteAsync("你好,C#");
});
app.Run(async context =>
{
await context.Response.WriteAsync("你好,.NET");
});
程序运行的时候,只有第一个Run方法的调用会被执行,后面两次都不会被执行。因为遇到Run方法意味着HTTP请求管道的将被终结,并且将处理结果直接发回给客户端,不管Run后面还有没有插入新的中间件都不会执行了。
中间件的分支映射
添加到HTTP管道的中间件是默认响应根URL请求的,但在实际开发中,有时候需要在根URL下面通过子路径来区分不同的功能,即根据不同的子URL来调用不同的中间件。
分支映射有两种比较常见的场景。
一种用法是错误处理,例如,根URL是 http://abc.org ,可将 http://abc.org/errors 专用于错误处理,调用向客户端返回错误信息的中间件。
另一种用法是Web Socket,例如,http://abc.org/ws 分支可专用于Web Socket通信。
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// 主管道上的中间件,设置字符编码
app.Use(async (context, next) =>
{
context.Response.ContentType = "text/html;charset=UTF-8";
await next();
});
// 以下皆为管道分支的中间件
app.Map("/home", _app =>
{
_app.Run(async context =>
{
await context.Response.WriteAsync("主页");
});
})
.Map("/about", _app =>
{
_app.Run(async context =>
{
await context.Response.WriteAsync("关于本站");
});
})
.Map("/news", _app =>
{
_app.Run(async context =>
{
await context.Response.WriteAsync("新闻列表");
});
});
}
运行程序,可分别输入以下URL来测试 http://localhost:3125/home , http://localhost:3125/home , http://localhost:3125/news 。