换个角度学习ASP.NET Core中间件

中间件真面目

关于ASP.NET Core中间件是啥,简单一句话描述就是:用来处理HTTP请求和响应的一段逻辑,而且能够决定是否把请求传递到管道中的下一个中间件!shell

上面只是概念上的一种文字描述,那问题来了,中间件在程序中究竟是个啥❓api

一切仍是从IApplicationBuilder提及,没错,就是你们熟悉的Startup类里面那个Configure方法里面的那个IApplicationBuilder(有点绕😵,抓住重点就行)。app

IApplicationBuilder,应用构建者,听这个名字就能感觉它的核心地位,ASP.NET Core应用就是依赖它构建出来,看看它的定义:框架

public interface IApplicationBuilder
{
    //...省略部分代码...
    IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
    RequestDelegate Build();
}

Use方法用来把中间件添加到应用管道中,此时咱们已经看到中间件的真面目了,原来是一个委托,输入参数是RequestDelegate,返回也是RequestDelegate,其实RequestDelegate仍是个委托,以下:async

public delegate Task RequestDelegate(HttpContext context);

还记得中间件是干吗的吗?是用来处理http请求和响应的,即对HttpContext的处理,这里咱们能够看出来原来中间件的业务逻辑就是封装在RequestDelegate里面。ide

总结一下:ui

middleware就是Func<RequestDelegate, RequestDelegate>,输入的是下一个中间件的业务处理逻辑,返回的就是当前中间件的业务处理逻辑,并在其中决定要不要调用下个中间件!咱们代码实现一个中间件看看(可能和咱们平时用的不太同样,但它就是中间件最原始的形式!):this

//Startup.Configure方法中
Func<RequestDelegate, RequestDelegate> middleware1 = next => async (context) =>
           {
               //处理http请求

               Console.WriteLine("do something before invoke next middleware in middleware1");
               //调用下一个中间件逻辑,固然根据业务实际状况,也能够不调用,那此时中间件管道调用到此就结束来了!

               await next.Invoke(context);
               Console.WriteLine("do something after invoke next middleware in middleware1");
           };
//添加到应用中           

app.Use(middleware1);

跑一下瞅瞅,成功执行中间件!code

IIS Express is running.
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: E:\vs2019Project\WebApplication3\WebApplication3
do something before invoke next middleware in middleware1
do something after invoke next middleware in middleware1

中间件管道

经过上面咱们有没有注意到,添加中间时,他们都是一个一个独立的被添加进去,而中间件管道就是负责把中间件串联起来,实现下面的一个中间件调用流转流程:
component

如何实现呢?这个就是IApplicationBuilder中的Build的职责了,再次看下定义:

public interface IApplicationBuilder
{
 //...省略部分代码...
 IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
 RequestDelegate Build();
}

Build方法一顿操做猛如虎,主要干一件事把中间件串联起来,最后返回了一个 RequestDelegate,而这个就是咱们添加的第一个中间件返回的RequestDelegate

看下框架默认实现:

//ApplicationBuilder.cs
public RequestDelegate Build()
        {
            RequestDelegate app = context =>
            {
                // If we reach the end of the pipeline, but we have an endpoint, then something unexpected has happened.
                // This could happen if user code sets an endpoint, but they forgot to add the UseEndpoint middleware.
                var endpoint = context.GetEndpoint();
                var endpointRequestDelegate = endpoint?.RequestDelegate;
                if (endpointRequestDelegate != null)
                {
                    var message =
                        $"The request reached the end of the pipeline without executing the endpoint: '{endpoint.DisplayName}'. " +
                        $"Please register the EndpointMiddleware using '{nameof(IApplicationBuilder)}.UseEndpoints(...)' if using " +
                        $"routing.";
                    throw new InvalidOperationException(message);
                }

                context.Response.StatusCode = 404;
                return Task.CompletedTask;
            };

            foreach (var component in _components.Reverse())
            {
                app = component(app);
            }

            return app;
        }
  • Build方法里面定义了一个 RequestDelegate ,做为最后一个处理逻辑,例如返回404。

  • _components存储着添加的全部中间件

  • 中间件管道调度顺序,就是按照中间添加的顺序调用,因此中间件的顺序很重要,很重要,很重要!

  • 遍历_components,传入next RequestDelegate,获取当前RequestDelegate,完成管道构建!

中间件使用

在此以前,仍是提醒下,中间件最原始的使用姿式就是

IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);

下面使用的方式,都是对此方式的扩展!

Lamda方式

大多数教程里面都提到的方式,直接上代码:

//扩展方法
//IApplicationBuilder Use(this IApplicationBuilder app, Func<HttpContext, Func<Task>, Task> middleware)
app.Use(async (context, next) =>
           {
               Console.WriteLine("in m1");
               await next.Invoke();
               Console.WriteLine("out m1");
           });

扩展方法简化了中间件的使用,这个里面就只负责写核心逻辑,而后扩展方法中把它包装成Func<RequestDelegate, RequestDelegate>类型进行添加,不像原始写的那样复杂,咱们看下这个扩展方法实现,哈,原来就是一个简单封装!咱们只要专一在middleware里面写核心业务逻辑便可。

public static IApplicationBuilder Use(this IApplicationBuilder app, Func<HttpContext, Func<Task>, Task> middleware)
        {
            return app.Use(next =>
            {
                return context =>
                {
                    Func<Task> simpleNext = () => next(context);
                    return middleware(context, simpleNext);
                };
            });
        }

若是咱们定义中间件做为终端中间件(管道流转此中间件就结束了,再也不调用后面的中间件)使用时,上面只要不调用next便可。

固然咱们还有另一个选择,本身使用扩展Run方法,传入的参数就是RequestDelegate,仍是上代码:

//扩展方法
//public static void Run(this IApplicationBuilder app, RequestDelegate handler);
app.Run(async (context) =>
            {
                Console.WriteLine("in m3");
                await context.Response.WriteAsync("test22");
                Console.WriteLine("out m3");
            });

到此,咱们有没有发现上面的方式有些弊端,只能处理下简单逻辑,若是要依赖第三方服务,那可怎么办?

定义中间件类方式

使用中间件类,咱们只要按照约定的方式,即类中包含InvokeAsync方法,就能够了。

使用类后,咱们就能够注入咱们须要的第三方服务,而后完成更复杂的业务逻辑,上代码

//定义第三方服务
public interface ITestService
    {
        Task Test(HttpContext context);
    }
    public class TestService : ITestService
    {
        private int _times = 0;
        public Task Test(HttpContext context)
        {
           return context.Response.WriteAsync($"{nameof(TestService)}.{nameof(TestService.Test)} is called {++_times} times\n");
        }
    }
//添加到IOC容器
public void ConfigureServices(IServiceCollection services)
        {


            services.AddTransient<ITestService, TestService>();
        }
//中间件类,注入ITestService
public class CustomeMiddleware1
    {
        private int _cnt;
        private RequestDelegate _next;
        private ITestService _testService;
        public CustomeMiddleware1(RequestDelegate next, ITestService testService)
        {
            _next = next;
            _cnt = 0;
            _testService = testService;
        }
        public async Task InvokeAsync(HttpContext context)
        {
            await _testService?.Test(context);
            await context.Response.WriteAsync($"{nameof(CustomeMiddleware1)} invoked {++_cnt} times");

        }
    }
//添加中间件,仍是一个扩展方法,预知详情,请看源码
app.UseMiddleware<CustomeMiddleware1>();

运行一下,跑出来的结果以下,完美!

等一下,有没有发现上面有啥问题???❓

明明ITestService是以Transient注册到容器里面,应该每次使用都是新实例化的,那不该该被显示被调用 15 次啊!!!

这个时候咱们应该发现,咱们上面的全部方式添加的中间件的生命周期其实和应用程序是一致的,也就是说是只在程序启动的时候实例化一次!因此这里第三方的服务,而后以Transient方式注册到容器,但在中间件里面变现出来就是一个单例效果,这就为何咱们不建议在中间件里面注入DbContext了,由于DbContext咱们通常是以Scoped来用的,一次http请求结束,咱们就要释放它!

若是咱们就是要在中间件里面是有ITestService,并且仍是Transient的效果,怎么办?

实现IMiddleware接口

//接口定义
public interface IMiddleware
{    
    ask InvokeAsync(HttpContext context, RequestDelegate next);
}
//实现接口
public class CustomeMiddleware : IMiddleware
    {
        private int _cnt;
        private ITestService _testService;
        public CustomeMiddleware(ITestService testService)
        {
            _cnt = 0;
            _testService = testService;
        }
        public async Task InvokeAsync(HttpContext context, RequestDelegate next)
        {
            await _testService?.Test(context);
            await context.Response.WriteAsync($"{nameof(CustomeMiddleware)} invoked {++_cnt} times");

        }
    }
//添加中间件
app.UseMiddleware<CustomeMiddleware>();

运行一下,结果报错了... ,提示CustomeMiddleware没有注册!

InvalidOperationException: No service for type 'WebApplication3.CustomeMiddleware' has been registered.

经过报错信息,咱们已经知道,若是实现了IMiddleware接口的中间件,他们并非在应用启动时就实例化好的,而是每次都是从IOC容器中获取的,其中就是IMiddlewareFactory

来解析出对应类型的中间件的(内部就是调用IServiceProvider),了解到此,咱们就知道,此类中间件此时是须要以service的方式注册到IOC容器里面的,这样中间件就能够根据注册时候指定的生命周期方式来实例化,从而解决了咱们上一节提出的疑问了!好了,咱们注册下中间件服务

public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<CustomeMiddleware>();
            services.AddTransient<ITestService, TestService>();
        }

再次屡次刷新请求,返回都是下面的内容

TestService.Test is called 1 times
CustomeMiddleware invoked 1 times

结语

中间件存在这么多的使用方式,每个存在都是为了解决实际需求的,当咱们了解这些背景知识后,在后面本身使用时,就能更加的灵活!

相关文章
相关标签/搜索