理解ASP.NET Core 中间件(Middleware)

目录
  • 中间件
  • 中间件管道
    • Run
    • Use
    • UseWhen
    • Map
    • MapWhen
    • Run & Use & UseWhen & Map & Map
  • 编写中间件并激活
    • 基于约定的中间件
    • 基于工厂的中间件
    • 基于约定的中间件 VS 基于工厂的中间件

中间件

先借用微软官方文档的一张图:

可以看到,中间件实际上是一种配置在HTTP请求管道中,用来处理请求和响应的组件。它可以:

  • 决定是否将请求传递到管道中的下一个中间件
  • 可以在管道中的下一个中间件处理之前和之后进行操作

此外,中间件的注册是有顺序的,书写代码时一定要注意!

中间件管道

Run

该方法为HTTP请求管道添加一个中间件,并标识该中间件为管道终点,称为终端中间件。也就是说,该中间件就是管道的末尾,在该中间件之后注册的中间件将永远都不会被执行。所以,该方法一般只会书写在Configure方法末尾。

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello, World!");
        });
    }
}

Use

通过该方法快捷的注册一个匿名的中间件

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
        {
            // 下一个中间件处理之前的操作
            Console.WriteLine("Use Begin");

            await next();

            // 下一个中间件处理完成后的操作
            Console.WriteLine("Use End");
        });
    }
}

注意:

  • 1.如果要将请求发送到管道中的下一个中间件,一定要记得调用next.Invoke / next(),否则会导致管道短路,后续的中间件将不会被执行
  • 2.在中间件中,如果已经开始给客户端发送Response,请千万不要调用next.Invoke / next(),也不要对Response进行任何更改,否则,将抛出异常。
  • 3.可以通过context.Response.HasStarted来判断响应是否已开始。

以下都是错误的代码写法

错误1:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
        {
            await context.Response.WriteAsync("Use");
            await next();
        });

        app.Run(context =>
        {
            // 由于上方的中间件已经开始 Response,此处更改 Response Header 会抛出异常
            context.Response.Headers.Add("test", "test");
            return Task.CompletedTask;
        });
    }
}

错误2

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
        {
            await context.Response.WriteAsync("Use");

            // 即使没有调用 next.Invoke / next(),也不能在 Response 开始后对 Response 进行更改
            context.Response.Headers.Add("test", "test");
        });
    }
}

UseWhen

通过该方法针对不同的逻辑条件创建管道分支。需要注意的是:

进入了管道分支后,如果管道分支不存在管道短路或终端中间件,则会再次返回到主管道。

当使用PathString时,路径必须以“/”开头,且允许只有一个'/'字符

支持嵌套,即UseWhen中嵌套UseWhen等

支持同时匹配多个段,如 /get/user

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        // /get 或 /get/xxx 都会进入该管道分支
        app.UseWhen(context => context.Request.Path.StartsWithSegments("/get"), app =>
        {
            app.Use(async (context, next) =>
            {
                Console.WriteLine("UseWhen:Use");

                await next();
            });
        });

        app.Use(async (context, next) =>
        {
            Console.WriteLine("Use");

            await next();
        });

        app.Run(async context =>
        {
            Console.WriteLine("Run");

            await context.Response.WriteAsync("Hello World!");
        });
    }
}

当访问 /get 时,输出如下:

UseWhen:Use
Use
Run

如果你发现输出了两遍,别慌,看看是不是浏览器发送了两次请求,分别是 /get 和 /favicon.ico

Map

  • 通过该方法针对不同的请求路径创建管道分支。需要注意的是:
  • 一旦进入了管道分支,则不会再回到主管道。
  • 使用该方法时,会将匹配的路径从HttpRequest.Path 中删除,并将其追加到HttpRequest.PathBase中。
  • 路径必须以“/”开头,且不能只有一个'/'字符
  • 支持嵌套,即Map中嵌套Map、MapWhen(接下来会讲)等
  • 支持同时匹配多个段,如 /post/user
public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        // 访问 /get 时会进入该管道分支
        // 访问 /get/xxx 时会进入该管道分支
        app.Map("/get", app =>
        {
            app.Use(async (context, next) =>
            {
                Console.WriteLine("Map get: Use");
                Console.WriteLine($"Request Path: {context.Request.Path}");
                Console.WriteLine($"Request PathBase: {context.Request.PathBase}");

                await next();
            });

            app.Run(async context =>
            {
                Console.WriteLine("Map get: Run");

                await context.Response.WriteAsync("Hello World!");
            });

        });

        // 访问 /post/user 时会进入该管道分支
        // 访问 /post/user/xxx 时会进入该管道分支
        app.Map("/post/user", app =>
        {
            // 访问 /post/user/student 时会进入该管道分支
            // 访问 /post/user/student/1 时会进入该管道分支
            app.Map("/student", app =>
            {
                app.Run(async context =>
                {
                    Console.WriteLine("Map /post/user/student: Run");
                    Console.WriteLine($"Request Path: {context.Request.Path}");
                    Console.WriteLine($"Request PathBase: {context.Request.PathBase}");

                    await context.Response.WriteAsync("Hello World!");
                });
            });

            app.Use(async (context, next) =>
            {
                Console.WriteLine("Map post/user: Use");
                Console.WriteLine($"Request Path: {context.Request.Path}");
                Console.WriteLine($"Request PathBase: {context.Request.PathBase}");

                await next();
            });

            app.Run(async context =>
            {
                Console.WriteLine("Map post/user: Run");

                await context.Response.WriteAsync("Hello World!");
            });
        });
    }
}

当你访问 /get/user 时,输出如下:

Map get: Use
Request Path: /user
Request PathBase: /get
Map get: Run

当你访问 /post/user/student/1 时,输出如下:

Map /post/user/student: Run
Request Path: /1
Request PathBase: /post/user/student

其他情况交给你自己去尝试啦!

MapWhen

Map类似,只不过MapWhen不是基于路径,而是基于逻辑条件创建管道分支。注意事项如下:

  • 一旦进入了管道分支,则不会再回到主管道。
  • 当使用PathString时,路径必须以“/”开头,且允许只有一个'/'字符
  • HttpRequest.PathHttpRequest.PathBase不会像Map那样进行特别处理
  • 支持嵌套,即MapWhen中嵌套MapWhen、Map等
  • 支持同时匹配多个段,如 /get/user
public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        // /get 或 /get/xxx 都会进入该管道分支
        app.MapWhen(context => context.Request.Path.StartsWithSegments("/get"), app =>
        {
            app.MapWhen(context => context.Request.Path.ToString().Contains("user"), app =>
            {
                app.Use(async (context, next) =>
                {
                    Console.WriteLine("MapWhen get user: Use");

                    await next();
                });
            });

            app.Use(async (context, next) =>
            {
                Console.WriteLine("MapWhen get: Use");

                await next();
            });

            app.Run(async context =>
            {
                Console.WriteLine("MapWhen get: Run");

                await context.Response.WriteAsync("Hello World!");
            });
        });
    }
}

当你访问 /get/user 时,输出如下:

MapWhen get user: Use

可以看到,即使该管道分支没有终端中间件,也不会回到主管道。

Run & Use & UseWhen & Map & Map

一下子接触了4个命名相似的、与中间件管道有关的API,不知道你有没有晕倒,没关系,我来帮大家总结一下:

  • Run用于注册终端中间件,Use用来注册匿名中间件,UseWhenMapMapWhen用于创建管道分支。
  • UseWhen进入管道分支后,如果管道分支中不存在短路或终端中间件,则会返回到主管道。MapMapWhen进入管道分支后,无论如何,都不会再返回到主管道。
  • UseWhenMapWhen基于逻辑条件来创建管道分支,而Map基于请求路径来创建管道分支,且会对HttpRequest.PathHttpRequest.PathBase进行处理。

编写中间件并激活

上面已经提到过的RunUse就不再赘述了。

基于约定的中间件

“约定大于配置”,先来个约法三章:

  • 1.拥有公共(public)构造函数,且该构造函数至少包含一个类型为RequestDelegate的参数
  • 2.拥有名为InvokeInvokeAsync的公共(public)方法,必须包含一个类型为HttpContext的方法参数,且该参数必须位于第一个参数的位置,另外该方法必须返回Task类型。
  • 3.构造函数中的其他参数可以通过依赖注入(DI)填充,也可以通过UseMiddleware传参进行填充。

通过DI填充时,只能接收 Transient 和 Singleton 的DI参数。这是由于中间件是在应用启动时构造的(而不是按请求构造),所以当出现 Scoped 参数时,构造函数内的DI参数生命周期与其他不共享,如果想要共享,则必须将Scoped DI参数添加到Invoke/InvokeAsync来进行使用。

通过UseMiddleware传参时,构造函数内的DI参数和非DI参数顺序没有要求,传入UseMiddleware内的参数顺序也没有要求,但是我建议将非DI参数放到前面,DI参数放到后面。(这一块感觉微软做的好牛皮)

  • 4.Invoke/InvokeAsync的其他参数也能够通过依赖注入(DI)填充,可以接收 Transient、Scoped 和 Singleton 的DI参数。

一个简单的中间件如下:

public class MyMiddleware
{
    // 用于调用管道中的下一个中间件
    private readonly RequestDelegate _next;

    public MyMiddleware(
        RequestDelegate next,
        ITransientService transientService,
        ISingletonService singletonService)
    {
        _next = next;
    }

    public async Task InvokeAsync(
        HttpContext context,
        ITransientService transientService,
        IScopedService scopedService,
        ISingletonService singletonService)
    {
        // 下一个中间件处理之前的操作
        Console.WriteLine("MyMiddleware Begin");

        await _next(context);

        // 下一个中间件处理完成后的操作
        Console.WriteLine("MyMiddleware End");
    }
}

然后,你可以通过UseMiddleware方法将其添加到管道中

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseMiddleware<MyMiddleware>();
    }
}

不过,一般不推荐直接使用UseMiddleware,而是将其封装到扩展方法中

public static class AppMiddlewareApplicationBuilderExtensions
{
    public static IApplicationBuilder UseMy(this IApplicationBuilder app) => app.UseMiddleware<MyMiddleware>();
}

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseMy();
    }
}

基于工厂的中间件

优势:

  • 按照请求进行激活。这个就是说,上面基于约定的中间件实例是单例的,但是基于工厂的中间件,可以在依赖注入时设置中间件实例的生命周期。
  • 使中间件强类型化(因为其实现了接口IMiddleware

该方式的实现基于IMiddlewareFactoryIMiddleware。先来看一下接口定义:

public interface IMiddlewareFactory
{
    IMiddleware? Create(Type middlewareType);

    void Release(IMiddleware middleware);
}

public interface IMiddleware
{
    Task InvokeAsync(HttpContext context, RequestDelegate next);
}

你有没有想过当我们调用UseMiddleware时,它是如何工作的呢?事实上,UseMiddleware扩展方法会先检查中间件是否实现了IMiddleware接口。 如果实现了,则使用容器中注册的IMiddlewareFactory实例来解析该IMiddleware的实例(这下你知道为什么称为“基于工厂的中间件”了吧)。如果没实现,那么就使用基于约定的中间件逻辑来激活中间件。

注意,基于工厂的中间件,在应用的服务容器中一般注册为 Scoped 或 Transient 服务

这样的话,咱们就可以放心的将 Scoped 服务注入到中间件的构造函数中了。

接下来,咱们就来实现一个基于工厂的中间件:

public class YourMiddleware : IMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        // 下一个中间件处理之前的操作
        Console.WriteLine("YourMiddleware Begin");

        await next(context);

        // 下一个中间件处理完成后的操作
        Console.WriteLine("YourMiddleware End");
    }
}

public static class AppMiddlewareApplicationBuilderExtensions
{
    public static IApplicationBuilder UseYour(this IApplicationBuilder app) => app.UseMiddleware<YourMiddleware>();
}

然后,在ConfigureServices中添加中间件依赖注入

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddTransient<YourMiddleware>();
    }
}

最后,在Configure中使用中间件

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseYour();
    }
}

微软提供了IMiddlewareFactory的默认实现:

public class MiddlewareFactory : IMiddlewareFactory
{
    // The default middleware factory is just an IServiceProvider proxy.
    // This should be registered as a scoped service so that the middleware instances
    // don't end up being singletons.
    // 默认的中间件工厂仅仅是一个 IServiceProvider 的代理
    // 该工厂应该注册为 Scoped 服务,这样中间件实例就不会成为单例
    private readonly IServiceProvider _serviceProvider;

    public MiddlewareFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public IMiddleware? Create(Type middlewareType)
    {
        return _serviceProvider.GetRequiredService(middlewareType) as IMiddleware;
    }

    public void Release(IMiddleware middleware)
    {
        // The container owns the lifetime of the service
        // DI容器来管理服务的生命周期
    }
}

可以看到,该工厂使用过DI容器来解析出服务实例的。因此,当使用基于工厂的中间件时,是无法通过UseMiddleware向中间件的构造函数传参的。

基于约定的中间件 VS 基于工厂的中间件

  • 基于约定的中间件实例都是 Singleton;而基于工厂的中间件实例可以是 Singleton、Scoped 和 Transient(当然,不建议注册为 Singleton)
  • 基于约定的中间件实例构造函数中可以通过依赖注入传参,也可以用过UseMiddleware传参;而基于工厂的中间件只能通过依赖注入传参
  • 基于约定的中间件实例可以在Invoke/InvokeAsync中添加更多的依赖注入参数;而基于工厂的中间件只能按照IMiddleware的接口定义进行实现。

到此这篇关于理解ASP.NET Core 中间件(Middleware)的文章就介绍到这了,更多相关ASP.NET Core Middleware内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 深入探究ASP.NET Core Startup初始化问题

    前言 Startup类相信大家都比较熟悉,在我们使用ASP.NET Core开发过程中经常用到的类,我们通常使用它进行IOC服务注册,配置中间件信息等.虽然它不是必须的,但是将这些操作统一在Startup中做处理,会在实际开发中带来许多方便.当我们谈起Startup类的时候你有没有好奇过以下几点 为何我们自定义的Startup可以正常工作. 我们定义的Startup类中ConfigureServices和Configure只能叫这个名字才能被调用到吗? 在使用泛型主机(IHostBuilder)

  • ASP.NET Core使用HostingStartup增强启动操作方法详解

    概念 在ASP.NET Core中我们可以使用一种机制来增强启动时的操作,它就是HostingStartup.如何叫"增强"操作,相信了解过AOP概念的同学应该都非常的熟悉.我们常说AOP使用了关注点分离的方式,增强了对现有逻辑的操作.而我们今天要说的HostingStartup就是为了"增强"启动操作,这种"增强"的操作甚至可以对现有的程序可以做到无改动的操作.例如,外部程序集可通过HostingStartup实现为应用提供配置服务.注册服务或

  • 理解ASP.NET Core 启动类(Startup)

    目录 准备工作:一份ASP.NET Core Web API应用程序 Startup类 Startup构造函数 ConfigureServices Configure 省略Startup类 IStartupFilter IHostingStartup HostingStartup 程序集 HostingStartup 特性 激活HostingStarup程序集 1.使用环境变量(推荐) 2.在程序中配置 多环境配置 环境配置方式 基于环境的 Startup 1.将IWebHostEnvironm

  • 理解ASP.NET Core 中间件(Middleware)

    目录 中间件 中间件管道 Run Use UseWhen Map MapWhen Run & Use & UseWhen & Map & Map 编写中间件并激活 基于约定的中间件 基于工厂的中间件 基于约定的中间件 VS 基于工厂的中间件 中间件 先借用微软官方文档的一张图: 可以看到,中间件实际上是一种配置在HTTP请求管道中,用来处理请求和响应的组件.它可以: 决定是否将请求传递到管道中的下一个中间件 可以在管道中的下一个中间件处理之前和之后进行操作 此外,中间件的注

  • 详解ASP.NET Core中间件Middleware

    本文为官方文档译文,官方文档现已非机器翻译 https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-2.1 什么是中间件(Middleware)? 中间件是组装到应用程序管道中以处理请求和响应的软件. 每个组件: 选择是否将请求传递给管道中的下一个组件. 可以在调用管道中的下一个组件之前和之后执行工作. 请求委托(Request delegates)用于构建请求管道,处理每个HTT

  • ASP.NET Core中间件初始化的实现

    前言 在日常使用ASP.NET Core开发的过程中我们多多少少会设计到使用中间件的场景,ASP.NET Core默认也为我们内置了许多的中间件,甚至有时候我们需要自定义中间件来帮我们处理一些请求管道过程中的处理.接下来,我们将围绕着以下几个问题来简单探究一下,关于ASP.NET Core中间件是如何初始化的 首先,使用UseMiddleware注册自定义中间件和直接Use的方式有何不同 其次,使用基于约定的方式定义中间件和使用实现IMiddleware接口的方式定义中间件有何不同 再次,使用基

  • 理解ASP.NET Core 错误处理机制(Handle Errors)

    目录 使用中间件进行错误处理 开发人员异常页 IDeveloperPageExceptionFilter 异常处理程序 通过lambda提供异常处理程序 异常处理程序页 无响应正文的Http错误状态码处理 UseStatusCodePages UseStatusCodePagesWithRedirects UseStatusCodePagesWithReExecute 使用过滤器进行错误处理 错误处理中间件 VS 异常过滤器 注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博

  • ASP.NET Core中间件

    目录 1.前言 2.使用中间件 2.1 Run 2.2 Use 2.3 Map和MapWhen 3.顺序 4.编写中间件(重点) 4.1中间件类 4.2中间件扩展方法 5.按每次请求创建依赖注入(DI) 1.前言 整个HTTP Request请求跟HTTP Response返回结果之间的处理流程是一个请求管道(request pipeline).而中间件(middleware)则是一种装配到请求管道以处理请求和响应的组件.每个组件: 可选择是否将请求传递到管道中的下一个组件. 可在管道中的下一个

  • 利用Asp.Net Core的MiddleWare思想如何处理复杂业务流程详解

    前言 最近利用Asp.Net Core 的MiddleWare思想对公司的古老代码进行重构,在这里把我的设计思路分享出来,希望对大家处理复杂的流程业务能有所帮助. 背景 一个流程初始化接口,接口中根据传入的流程类型,需要做一些不同的工作. 1.有的工作是不管什么类型的流程都要做的(共有),有的工作是某一流程特有的. 2.各个处理任务基本不存在嵌套关系,所以代码基本是流水账式的. 3.流程的种类较多,代码中if或者switch判断占了很大的篇幅. 4.这些处理工作大致可分为三大类,前期准备工作(参

  • ASP.NET Core中间件实现限流的代码

    目录 一.限流算法 1.计数器算法 1.1固定窗口算法 1.2滑动窗口算法 2.令牌桶算法 3.漏桶算法 二.ASP.NETCore中间件实现限流 1.中间件代码 2.在管道中的使用 一.限流算法 在高并发系统中,有三把利器用来保护系统:缓存.降级和限流. 本文主要是介绍限流,限流算法主要有以下三种: 1.计数器算法 固定窗口 滑动窗口 2.令牌桶算法 3.漏桶算法 1.计数器算法 1.1 固定窗口算法 计数器算法是限流算法里最简单也是最容易实现的一种算法.比如我们规定,对于A接口来说,我们1分

  • ASP.NET Core中间件用法与官方常用中间件介绍

    目录 一.什么是中间件 中间件和过滤器的区别 二.中间件常用方法 1.Run方法 2.Use方法 3.Map方法 4.Mapwhen方法 三.自定义中间件 四.官方常用中间件 1.异常处理中间件 2.HTTPS重定向中间件 3.静态文件中间件 4.Cookie中间件 5.路由中间件 6.身份认证中间件 7.授权中间件 8.会话中间件 9.终结点路由中间件 一.什么是中间件 我们都知道,任何的一个web框架都是把http请求封装成一个管道,每一次的请求都是经过管道的一系列操作,最终才会到达我们写的

  • 理解ASP.NET Core 依赖注入(Dependency Injection)

    目录 依赖注入 什么是依赖注入 依赖注入有什么好处 ASP.NET Core内置的依赖注入 服务生存周期 服务释放 TryAdd{Lifetime}扩展方法 解析同一服务的多个不同实现 Replace && Remove 扩展方法 Autofac 服务解析和注入 构造函数注入 方法注入 属性注入 一些注意事项 框架默认提供的服务 依赖注入 什么是依赖注入 简单说,就是将对象的创建和销毁工作交给DI容器来进行,调用方只需要接收注入的对象实例即可. 微软官方文档-DI 依赖注入有什么好处 依赖

随机推荐