ASP.NET Core/MVC (及自定义中间件编写) 入门教程






4.81/5 (9投票s)
在本文中,我们将尝试理解 ASP.NET Core 中间件的概念。
引言
在本文中,我们将尝试理解 ASP.NET Core 中间件的概念。我们将了解中间件在请求-响应管道中的重要作用,以及我们如何编写和集成自己的自定义中间件。
背景
在我们深入了解中间件是什么以及它带来的价值之前,我们需要理解经典 ASP.NET 模型中请求-响应是如何工作的。在早期,ASP.NET 中的请求和响应对象非常庞大,并且与 IIS 存在非常紧密的耦合。这是一个问题,因为这些对象中的某些值是由 IIS 请求-响应管道填充的,而对这些庞大的对象进行单元测试是一个巨大的挑战。
因此,需要解决的第一个问题是将应用程序与 Web 服务器解耦。这由社区拥有的标准 Open Web Interface for .NET (OWIN) 很好地定义了。由于旧的 ASP.NET 应用程序依赖于 `System.Web` DLL,而该 DLL 内部与 IIS 紧密耦合,因此将应用程序与 Web 服务器解耦非常困难。为了规避这个问题,OWIN 定义了去除 Web 应用程序对 `System.web` 程序集的依赖,从而实现与 Web 服务器 (IIS) 的解耦。
OWIN 规范主要定义了以下参与者:
- 服务器 — 直接与客户端通信的 HTTP 服务器,然后使用 OWIN 语义来处理请求。服务器可能需要一个适配器层来转换为 OWIN 语义。
- Web 框架 — OWIN 之上一个独立的组件,公开其自己的对象模型或 API,应用程序可以使用它们来促进请求处理。Web 框架可能需要一个适配器层来从 OWIN 语义进行转换。
- Web 应用程序 — 一个特定的应用程序,可能构建在 Web 框架之上,它使用 OWIN 兼容的服务器运行。
- 中间件 — 连接服务器和应用程序之间的管道组件,用于检查、路由或修改特定用途的请求和响应消息。
- 宿主 — 应用程序和服务器在其内部执行的进程,主要负责应用程序的启动。某些服务器也是宿主。
由于 OWIN 仅仅是一个标准,近年来有多种实现,从 Katana 到 ASP.NET Core 中的当前实现。现在我们将重点关注 ASP.NET Core 中的中间件实现。
在此之前,让我们尝试理解什么是中间件。对于来自 ASP.NET 世界的开发人员来说,`HTTPModule` 和 `HTTPHander` 的概念相当熟悉。它们用于拦截请求-响应管道并通过编写自定义模块或处理程序来实现我们的自定义逻辑。在 OWIN 世界中,中间件实现了相同的功能。
OWIN 规定,从 Web 服务器到 Web 应用程序的请求必须以管道的形式经过多个组件,每个组件都可以检查、重定向、修改或为这个传入的请求提供响应。然后,响应将以相反的顺序传递回 Web 服务器,Web 服务器然后可以将其提供给用户。下图直观地展示了这个概念。
如果我们查看上图,可以看到请求经过一系列中间件,然后一些中间件决定为请求提供响应,然后响应沿着相同的中间件链(请求时经过的所有中间件)返回到 Web 服务器。因此,中间件通常可以:
- 处理请求并生成响应
- 监控请求并将其传递给管道中的下一个中间件
- 监控请求,对其进行修改,然后将其传递给管道中的下一个中间件
如果我们尝试根据上述实际用例来查找中间件:
- 处理请求并生成响应:MVC 本身就是一个中间件,通常在中间件管道的最后进行配置。
- 监控请求并将其传递给下一个中间件:日志记录中间件,它只需记录请求和响应的详细信息。
- 监控请求,对其进行修改,然后将其传递给下一个中间件:路由和身份验证模块,其中我们监控请求并决定调用哪个控制器(路由),并可能更新身份和主体信息以进行授权(身份验证-授权)。
Using the Code
在本文中,我们将创建 2 个 OWIN 中间件。第一个将演示我们不修改请求的场景。为此,我们将简单地将请求和响应时间记录到日志中 - `TimingMiddleware`。第二个将检查传入的响应,查找特定的标头值以确定哪个租户正在调用代码,然后如果租户无效则返回 - `MyTenantValidator`。
注意:在开始示例实现之前,值得强调的是,中间件是管道和过滤器模式的实现。管道和过滤器模式指出,如果我们想执行一个涉及一系列独立活动的复杂处理,最好将每个活动分离为一个可重用的独立任务。这在可重用性、性能和可伸缩性方面提供了优势。
让我们开始看看中间件类定义应该是什么样子。定义自定义中间件有两种方法:
- 自定义中间件类
- 内联自定义中间件
自定义中间件类
第一种方法是有一个包含中间件逻辑的自定义类。
public class MyCustomMiddleware
{
private readonly RequestDelegate _next;
public MyCustomMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// Todo: Our logic that we need to put in when the request is coming in
// Call the next delegate/middleware in the pipeline
await _next(context);
// Todo: Our logic that we need to put in when the response is going back
}
}
这个类所做的是,当请求到达此中间件时就会被调用。 `InvokeAsync` 函数将被调用,并将当前的 `HttpContext` 传递给它。然后,我们可以使用此上下文执行自定义逻辑,然后调用管道中的下一个中间件。一旦请求由此中间件之后的其他中间件处理完毕,响应就会生成,并且响应将沿着反向链返回,函数将在我们的 `_next` 调用之后到达,在那里我们可以放置我们想要在响应返回到前一个中间件之前执行的逻辑。
为了让我们的中间件进入管道,我们需要在 `Startup` 类的 `Configure` 方法中使用它来挂接我们的中间件。
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// OUR CUSTOM MIDDLEWARE
app.UseMiddleware<MyCustomMiddleware>();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
上面的代码显示了我们如何将自定义中间件作为管道中的第一个中间件挂接。中间件将按照它们在此方法中挂接的顺序被调用。因此,在上面的代码中,我们的中间件将首先被调用,而 MVC 中间件将是最后一个被调用的。
内联自定义中间件
内联自定义中间件直接在 `Configure` 方法中定义。以下代码显示了如何实现这一点:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// OUR CUSTOM MIDDLEWARE
app.Use(async (context, next) =>
{
// Todo: Our logic that we need to put in when the request is coming in
// Call the next delegate/middleware in the pipeline
await next();
// Todo: Our logic that we need to put in when the response is going back
});
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
两种方法的最终结果是相同的。因此,如果我们的中间件只做一些琐碎的事情,并且将其作为内联放在代码中不会影响代码的可读性,那么我们可以创建内联自定义中间件。如果我们要添加的代码在我们的中间件中有重要的代码和逻辑,那么我们应该使用自定义中间件类来定义我们的中间件。
回到我们将要实现的中间件,我们将使用内联方法来定义 `TimingMiddleware`,并使用自定义类方法来定义 `MyTenantValidator`。
实现 TimingMiddleware
此中间件的唯一目的是检查请求和响应,并记录当前请求处理所花费的时间。让我们将其定义为内联中间件。以下代码显示了如何做到这一点。
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.Use(async (context, next) =>
{
DateTime startTime = DateTime.Now;
// Call the next delegate/middleware in the pipeline
await next();
DateTime endTime = DateTime.Now;
TimeSpan responseTime = endTime - startTime;
// Log the response time here using your favorite logging or telemetry module
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
我们将此中间件挂接在 MVC 中间件之前,以便我们可以衡量请求处理所需的时间。它被放置在 `UseStaticFiles` 中间件之后,这样该中间件就不会被我们应用程序提供的所有静态文件调用。
实现 MyTenantValidator
现在让我们实现一个处理租户验证的中间件。它将检查传入的标头,如果租户无效,它将停止请求处理。
注意:出于简单起见,我将查找一个硬编码的租户 ID 值。但在实际应用程序中,绝不应使用此方法。这样做仅用于演示目的。请注意,我们将使用租户 ID 值 `12345678`。
此中间件将在其单独的类中编写。逻辑很简单,检查传入请求中的标头。如果标头与硬编码的租户 ID 匹配,则让请求继续到下一个中间件,否则通过此中间件自身发送响应来终止请求。让我们看看这个中间件的代码。
public class MyTenantValidator
{
private readonly RequestDelegate _next;
public MyTenantValidator(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
StringValues authorizationToken;
context.Request.Headers.TryGetValue("x-tenant-id", out authorizationToken);
if(authorizationToken.Count > 0 && authorizationToken[0] == "12345678")
{
// Call the next delegate/middleware in the pipeline
await _next(context);
}
else
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
await context.Response.WriteAsync("Invalid calling tenant");
return;
}
}
}
现在,让我们在 `Startup` 类中注册这个中间件。
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMiddleware<MyTenantValidator>();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.Use(async (context, next) =>
{
DateTime startTime = DateTime.Now;
// Call the next delegate/middleware in the pipeline
await next();
DateTime endTime = DateTime.Now;
TimeSpan responseTime = endTime - startTime;
// Log the response time here using your favorite logging or telemetry module
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
有了这段代码,如果我们尝试运行应用程序,我们将看到错误响应。
为了规避此问题,我们需要在标头中传递租户 ID。
有了这个更改,当我们再次访问应用程序时,应该就可以浏览我们的应用程序了。
注意:尽管我们在 ASP.NET Core 的上下文中进行讨论,但在所有遵循 OWIN 标准的 MVC 实现中,中间件的概念都是相同的。
关注点
在本文中,我们讨论了 ASP.NET Core 中间件。我们了解了什么是中间件以及如何编写自己的自定义中间件。本文是从初学者的角度编写的。希望它能提供一些信息。
参考文献
历史
- 2018 年 9 月 12 日:首次发布