目录
  • 中间件
  • 中间件管道
    • 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内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!