3. 中间件的最佳方法

中间件是 ASP.NET Core 中最强大的概念之一。对于传统的 ASP.NET 开发人员来说,“中间件”是一个相对较新的术语。在中间件出现之前,存在 HTTP 处理程序和模块,需要通过 web.config 进行单独的代码配置。现在,中间件被视为 ASP.NET 应用程序中的一等公民,使其更易于在单个代码库中维护。通用请求和响应概念首次在 ASP.NET Core 1.0 中引入,被视为应用程序的管道,能够控制请求和响应的主体。这为创建 ASP.NET Core Web 应用程序的惊人功能开辟了许多可能性。

在本章的开头,我们将研究如何使用中间件以及几乎每个 ASP.NET Core 应用程序中的一些常见内置中间件组件。接下来,我们将研究三个请求委托(RunMapUse),并解释每个委托在管道中的用途。我们还将介绍一些清理中间件的方法,并最终将这些概念应用于构建一个简单的中间件示例。

在本章中,我们将介绍以下主要主题:

  • 使用中间件
  • 中间件的常见做法
  • 创建表情符号中间件组件

在本章结束时,您将理解中间件的工作原理、编写自己的中间件时如何使用请求委托和标准,并理解如何创建自己的中间件组件。

技术要求

由于这是包含技术要求的第一章(由于我们现在处于编码领域,因此接下来还有许多章),因此选择支持 ASP.NET Core 7.0 或更高版本和 C# 代码的最喜欢的编辑器将是理想的选择。我最喜欢的三个编辑器如下:

  • Visual Studio(最好是 2022 或更新版本)
  • Visual Studio Code
  • JetBrains Rider

我们将使用的编辑器是 Visual Studio 2022 Enterprise,但任何版本(社区版或专业版)都适用于本章。

本章的代码位于 Packt Publishing 的 GitHub 存储库中:https://github.com/PacktPublishing/ASP.NET-Core-8-Best-Practices。

使用中间件

中间件是应用程序启动时在应用程序开始时配置的软件。需要注意的是,您添加的中间件应基于应用程序的要求。没有必要添加每个组件。简化中间件管道很重要,我们将很快讨论这一点。

有人说,库和框架之间的区别在于,库是您从应用程序调用的代码,而框架则以某种方式构造来调用您的代码。这就是中间件从早期版本的 ASP.NET 发展而来的样子。

在本节中,我们将介绍中间件管道的常见流程以及如何控制中间件组件中发生的事情。在本节结束时,您将理解中间件管道的工作原理。

理解中间件管道

当您的 Web 应用程序启动时,中间件在每个应用程序生命周期中被调用和构建一次。一旦中间件组件被注册,它们就会按特定顺序执行。这个顺序在整个管道中都很重要,因为每个中间件组件都可以依赖先前注册的组件。

例如,在配置授权组件之前,身份验证组件很重要,因为我们需要先知道某人是谁,然后才能确定他们可以做什么。

在图 3.1 中,我们可以看到 Web 应用程序中标准中间件管道的组成,接下来我们将介绍:

图 3.1 – ASP.NET 8 Web 应用程序的标准中间件管道

在这里插入图片描述

这些组件中的每一个都是可选的,但一些中间件组件依赖于其他组件。当用户请求 URL 时,第一个中间件组件被命中。在本例中,它是 ExceptionHandler。一旦 ExceptionHandler 完成,管道就会转到下一个组件,即 HSTS 组件。当我们移动每个中间件组件时,我们最终到达端点。处理完端点后,响应将以相反的顺序通过中间件管道发回。

正如本节开头所述,您的中间件取决于您的应用程序在添加其他组件时需要什么。如果您的应用程序是单页应用程序 (SPA),则包含 CORS、静态文件和路由中间件将非常重要。

每个中间件组件负责根据您的配置将信息传递给下一个组件或终止该过程。如果它们决定终止管道,则它们被称为终端中间件组件。它们会故意阻止中间件处理任何其他请求并退出管道。

使用请求委托 - 运行、使用和映射

通过我们到目前为止讨论的所有内容,您可能想知道我们如何创建管道。

可用的三个请求委托是RunUseMap扩展方法。您无疑在Program.cs代码中多次使用它们,但这三个之间有什么区别?

Run

Run()请求委托是严格的终端中间件,这意味着它将运行并立即退出管道。它不包含next参数。它只是运行并立即终止管道。

如果我们查看以下代码,这将立即终止管道的执行:

app.Run(async context =>
{
    await context.Response.WriteAsync("This will terminate the web  app.");
});

请注意,委托中没有引入 next 参数。上述代码将向浏览器写入消息 “这将终止 Web 应用程序。”,并立即终止管道。

Use

Use() 请求委托用于在管道中将多个请求委托链接在一起。

实现正确的 Use 请求委托的关键是使用 await next.Invoke()next.Invoke() 将按顺序执行下一个中间件组件。此行之前的任何内容都将在请求中处理,此行之后的任何内容都将在返回给用户的响应中处理。

让我们看看以下代码片段中两个匿名中间件组件的代码示例:

app.Use(async (context, next) =>
{
    await context.Response.WriteAsync("In the first middleware call.\        r\n");
    await context.Response.WriteAsync("Executing the next         Middleware...\r\n");
    await next();
    await context.Response.WriteAsync("In the first middleware call…on the return trip.\r\n");
});
app.Use(async (context, next) =>
{
    await context.Response.WriteAsync("We're in the second middleware         call\r\n");
    await next();
    await context.Response.WriteAsync("On our way back from the second         middleware call\r\n");
});

此代码创建以下输出:

In the first middleware call.
Executing the next Middleware...
We're in the second middleware call
On our way back from the second middleware call
In the first middleware call…on the return trip.

您会注意到在执行 next.invoke() 代码行之前发生的任何情况,然后执行将按顺序转到下一个中间件。一旦我们到达中间件管道的末尾,我们就会返回,在每个中间件的 await next(); 语句之后执行所有代码。

执行每个中间件组件后,应用程序将运行,然后按相反顺序返回。

Map

Map() 请求委托用于根据特定请求路径或路由对管道进行分支。虽然这是针对特定中间件条件的,但创建新映射的可能性极小。通常最好使用预构建的中间件组件,例如 .MapRazorPages().MapControllers() 或任何其他 .MapXxxx() 方法。这些方法已经具有预定义的路由。大多数路由发生在其他扩展中,例如前面提到的中间件方法。

还有一个 MapWhen() 扩展方法,用于根据给定谓词的结果进行条件中间件分支。例如,如果您想为您的网站创建一个受控的维护页面,您可以使用一个名为 underMaintenance 的简单布尔值,并使用它来显示一条简单消息,直到您的网站再次可用:

app.MapWhen(_ => underMaintenance, ctx =>
    ctx.Run(async context =>
    {
        await context.Response
            .WriteAsync("We are currently under maintenance.");
    })
);

在前面的代码中,我们添加了 .MapWhen() 委托,以使用特定的布尔值来标识我们是否处于维护状态。请注意,我们使用 .Run 委托,因为我们不想继续执行中间件管道。这种方法只是中间件灵活性的一个例子。

使用内置中间件组件

虽然您可以创建自己的中间件组件,但最好的方法是从大量现有的内置组件中查看是否存在中间件组件。完整列表位于 https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-7.0#built-in-middleware。此图表提供了每个中间件组件的描述以及将其放置在中间件管道中的位置。除了内置组件之外,还可以使用 NuGet 查找创新的中间件组件。

在本节中,我们介绍了中间件管道,理解了如何使用请求委托以及每个请求委托可以做什么,并理解了 ASP.NET Web 应用程序可用的所有内置中间件组件。在下一节中,我们将研究使用中间件的常见做法。

中间件的常见做法

在本节中,我们将回顾编写自己的中间件时的一些常见做法,以使 Web 应用程序中的所有内容都能以最佳方式运行。让我们开始吧!

推迟到异步

在使用中间件时,我们希望获得尽可能好的性能,以便用户可以开始在应用程序中工作。随着越来越多的用户继续使用该应用程序,性能可能会受到影响。

同步操作是执行代码并且应用程序必须等待它完成的地方,这意味着它是单线程的并在应用程序的主线程上运行,但是当执行异步操作时,它会创建一个新线程并让框架知道在完成处理后要调用什么。这通过 async/await 关键字表示。

对于大多数中间件操作,最好在适用时使用异步调用。这将提高中间件(和应用程序)的性能以及更好的可扩展性和响应能力。

确定顺序的优先级

设置中间件的一个更重要的要点是确认一切都按正确的顺序进行。

将应用程序的要求与上一个图表进行比较,以确定您需要哪些中间件组件以及它们在您的 Web 应用程序中的正确顺序。

例如,如果您想包含 W3C 日志记录中间件组件(包含在 Microsoft 的内置中间件组件中),它必须位于管道的开头,以记录整个应用程序中发出的任何后续请求。每个组件在管道中都有自己的位置。

整合现有中间件

当您创建新的 ASP.NET 项目时,您会注意到 Program.cs 中列出的 app.UseXxx() 集合。虽然这是准备管道的“开箱即用”方法,但还有其他方法可以组织和注册应用程序的组件。

一种方法是根据您如何将用途逻辑划分为类似的组,同时保持组件的顺序不变,来使用扩展方法。

一个例子是将所有客户端中间件移至其自己的扩展方法 .UseClientOptions():

public static class WebApplicationExtensions
{
    public static void UseClientOptions(this WebApplication app)
    {
        app.UseHttpsRedirection();
        app.UseStaticFiles();
    }
}

现在,您的 Program.cs 文件中的代码包含一行,并且您确切地知道扩展方法的作用:

app.UseClientOptions();

使用这种方法时,您的 Program.cs 文件会更干净、维护得更好,并且包含的代码行更少。

其他可能需要分区的区域如下:

  • UseDataXxxxx() – 应用程序连接字符串的集中位置
  • UseMapping()/UseRouting() – 为您的应用程序和 API 创建路由集合
  • RegisterDependencyInjection() – 将类集中在多个扩展方法中,类似于此分组方法,但按应用程序中的部分进行分区 - 例如,RegisterDIPayroll() 用于注册与应用程序的 Payroll 部分相关的类

虽然这些只是建议,但其概念是缩减 Program.cs 文件的大小,以便其他开发人员用更少的代码行理解该方法,并为其他开发人员提供足够的清晰度以进一步扩展该技术。

作为建议,请预先包含所有重要的中间件组件,并确认应用程序按预期运行,然后通过创建要合并的组来执行重构。请记住,中间件组件的顺序很重要。

封装中间件

创建第一个中间件组件时,您可能想按以下方式创建和使用它:

app.Use(async (context, next) =>
{
    app.Logger.LogInformation("In our custom Middleware...");
    // Prepare work for when we write to the Response
    await next();
    // work that happens when we DO write to the response.
});

这种方法的一个问题是,如果您有大量自定义中间件组件,上述代码可能会使您的 Program.cs 文件看起来有点混乱。

一旦您的自定义组件开始工作,最好将其封装到自己的类中以提高可重用性。如果我们使用前面的示例,我们的新类将如下所示:

public class MyFirstMiddleware
{
    private readonly ILogger _logger;
    private readonly RequestDelegate _next;
    public MyFirstMiddleware(ILogger logger, RequestDelegate next)
    {
        _logger = logger;
        _next = next;
    }
    public async Task InvokeAsync(HttpContext context)
    {
        _logger.LogInformation("In our custom Middleware...");
        // Prepare work for when we write to the Response
        await _next(context);
        // work that happens when we DO write to the response.
    }
}

在此示例中,MyFirstMiddleware 组件是一个简单的类,它只能包含一个InvokeInvokeAsync 方法。如前所述,我们将使用InvokeAsync 异步方法。

如果您想知道ILogger 是如何传入的,ASP.NET Core 在其开箱即用的依赖项注入库中自动注册了许多类。ILogger 就是其中一个类,因此我们不必担心将其传递给我们的MyFirstMiddleware 组件。

我们可以像这样在Program.cs 文件中使用我们的类:

app.UseMiddleware<MyFirstMiddleware>();

但是,由于我们是优秀的 ASP.NET 开发人员,因此我们绝对可以改进代码。大多数中间件组件都附加了扩展方法,以使其更易于使用(我们现在将使用以下代码添加):

public static class MyFirstMiddlewareExtensions
{
    public static IApplicationBuilder UseMyFirstMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyFirstMiddleware>();
    }
}

我们的 Program.cs 文件现在更加简单和干净了:

app.UseMyFirstMiddleware();

这些简单的做法使开发人员的工作更易于重用和封装。

在本节中,我们介绍了一些编写可维护且高效的中间件的标准方法,包括使用异步调用、优先考虑组件的顺序以及将现有中间件整合到扩展方法中。我们还研究了如何通过创建类和扩展方法来封装组件,从而使代码更易于阅读。

创建表情符号中间件组件

随着表情符号的兴起……对不起,表情符号……在 2000 年代,许多旧式网站使用旧式的基于文本的表情符号,而不是更现代的表情符号。旧式内容管理系统CMS)的内容中必须包含大量这些基于文本的字符。更新网站内容以用适当的表情符号替换所有这些表情符号听起来非常耗时。

在本节中,我们将应用我们的标准来创建一个表情符号中间件组件,如果它检测到基于文本的表情符号,它会将其转换为更现代的表情符号。

封装中间件

有了这个新的中间件组件,我们希望在 EmojiMiddleware.cs 中的自己的类中创建它。

这是我们组件的初稿:

public class EmojiMiddleware
{
    private readonly ILogger _logger;
    private readonly RequestDelegate _next;
    public EmojiMiddleware(ILogger logger, RequestDelegate next)
    {
        _logger = logger;
        _next = next;
    }
    public async Task InvokeAsync(HttpContext context)
    {
        await _next(context);
    }
}
public static class EmojiMiddlewareExtensions
{
    public static IApplicationBuilder UseEmojiMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<EmojiMiddleware>();
    }
}

虽然这并不是很令人兴奋,但这个样板满足了前面提到的构建中间件组件的所有标准,包括以下内容:

  • 封装的中间件组件
  • 使用异步方法(InvokeAsync()
  • 用于重用和可读性的扩展方法

我们现在可以专注于转换过程。

检查组件的管道

在中间件中,有两种处理请求和响应的方法:使用流或管道。虽然管道是高性能的更好选择,但我们将重点关注 EmojiMiddleware 的流。我们将在后面的章节中检查管道。

我们的中间件流通过 HttpRequest.BodyHttpResponse.Body 位于 HttpContext 中。在我们的 Invoke 方法中,我们方便地传入 HttpContext

我们的首要任务是创建 EmojiStream。这将接受一个简单的响应流并将其读入内存。一旦我们有了 HTML,我们就可以搜索和替换表情符号。我们需要一个映射来识别基于文本的字符以及在 HTML 中用什么图像替换它们。

为了让我们的生活更轻松一些,我们将从 Stream 基类继承,并简单地覆盖特定方法以满足我们的需求。我们的 EmojiStream 类所需的唯一实现是我们基于文本的表情符号到表情符号的映射和 .Write() 方法,如以下代码所示:

public class EmojiStream: Stream
{
    private readonly Stream _responseStream;
    private readonly Dictionary<string, string> _map = new()
    {
        { ":-)", " :) " },
        { ":)", " :) " },
        { ";-)", " ;) " }
    };
    public EmojiStream(Stream responseStream)
    {
        ArgumentNullException.ThrowIfNull(responseStream);
        _responseStream = responseStream;
    }
    public override bool CanRead => _responseStream.CanRead;
    public override bool CanSeek => _responseStream.CanSeek;
    public override bool CanWrite => _responseStream.CanWrite;
    public override long Length => _responseStream.Length;
    public override long Position
    {
        get => _responseStream.Position;
        set => _responseStream.Position = value;
    }
    public override void Flush()
    {
        _responseStream.Flush();
    }
    public override int Read(byte[] buffer, int offset, int count)
    {
        return _responseStream.Read(buffer, offset, count);
    }
    public override long Seek(long offset, SeekOrigin origin)
    {
        return _responseStream.Seek(offset, origin);
    }
    public override void SetLength(long value)
    {
        _responseStream.SetLength(value);
    }
    public override void Write(byte[] buffer, int offset, int count)
    {
        var html = Encoding.UTF8.GetString(buffer, offset, count);
        foreach (var emoticon in _map)
        {
            if (!html.Contains(emoticon.Key)) continue;
            html = html.Replace(emoticon.Key, emoticon.Value);
        }
        buffer = Encoding.UTF8.GetBytes(html);
        _responseStream.WriteAsync(buffer, 0, buffer.Length);
    }
}

在代码的开头,我们创建了要在 HTML 中查找的表情符号映射。除了 WriteAsync() 方法之外,EmojiStream 类相当常见。我们将使用 GetString() 方法获取 HTML,并在响应中搜索每个表情符号。如果找到一个,我们将用图像标签替换它,最后将字节写回流中。

由于我们专注于在中间件中使用流,因此我们将把流传递给构造函数,而不是创建新实例。

剩下中间件部分后,我们可以在类中使用 EmojiStream

public class EmojiMiddleware
{
    private readonly RequestDelegate _next;
    public EmojiMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    public async Task InvokeAsync(HttpContext context)
    {
        using var buffer = new MemoryStream();
        // R用我们的缓冲区替换上下文响应
        var stream = context.Response.Body;
        context.Response.Body = buffer;
        // 如果有任何其他中间件组件,则调用管道的其余部分
        await _next(context);
        // 重置并读出内容
        buffer.Seek(0, SeekOrigin.Begin);
        // 调整响应流以包含我们的图像。
        var emojiStream = new EmojiStream(stream);
        // Reset the stream again
        buffer.Seek(0, SeekOrigin.Begin);
        // 将我们的内容复制到原始流并放回
        await buffer.CopyToAsync(emojiStream);
        context.Response.Body = emojiStream;
    }
}

虽然我们的中间件组件采用了一个简单的RequestDelegate,但组件的大部分内容都在InvokeAsync()方法中。首先,我们为响应创建一个新的流。接下来,我们用自己的流替换标准响应。当我们从端点返回时,我们创建EmojiStream实例并将自定义流传递给Response.Body

由于HttpContextHttpRequest.BodyHttpResponse.Body作为流公开,因此将HttpContext传递到自定义中间件组件中更加容易。

当然,我们不能忘记我们的扩展方法,如下所示:

public static class EmojiMiddlewareExtensions
{
    public static IApplicationBuilder UseEmojiMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<EmojiMiddleware>();
    }
}

此扩展方法被视为一种外观,用于隐藏我们的 EmojiStream 在幕后执行的操作的细节。虽然我们可以在 Program.cs 文件中使用 builder.UseMiddleware() 语法,但扩展方法会对其进行一些清理,使其看起来更专业。

最后需要做的是将 EmojiMiddleware 添加到 Program.cs 文件中的管道中:

app.UseEmojiMiddleware();

创建全新的 ASP.NET Core 网站后,我们将以下 HTML 添加到索引页的底部:

<div class="text-center">
    <h2>Smile, you're on candid camera. :-) :)</h2>
    <p>It even works inside ;-) a paragraph.</p>
</div>

当我们运行没有中间件组件的应用程序时,我们会得到以下输出(图 3.2):

图 3.2 – 在将 EmojiMiddleware 添加到管道之前

在这里插入图片描述

当我们将 Emoji Middleware 添加到管道并再次运行应用程序时,我们会收到以下输出(图 3.3):

图 3.3 – 将 EmojiMiddleware 添加到管道之后

在这里插入图片描述

在本节中,我们通过将逻辑封装在类中来构建第一个中间件组件,使用流检查组件管道,并在 Web 应用程序中使用中间件组件。

  • 35
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

0neKing2017

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值