1. 程式人生 > 其它 >ASP.NET Core 中介軟體(Middleware)的使用及其原始碼解析(一)

ASP.NET Core 中介軟體(Middleware)的使用及其原始碼解析(一)

中介軟體是一種裝配到應用管道以處理請求和響應的軟體。每個元件:

1、選擇是否將請求傳遞到管道中的下一個元件。

2、可在管道中的下一個元件前後執行工作。

請求委託用於生成請求管道。請求委託處理每個 HTTP 請求。

請求管道中的每個中介軟體元件負責呼叫管道中的下一個元件,或使管道短路。當中介軟體短路時,它被稱為“終端中介軟體”,因為它阻止中介軟體進一步處理請求。

廢話不多說,我們直接來看一個Demo,Demo的目錄結構如下所示:

本Demo的Web專案為ASP.NET Core Web 應用程式(目標框架為.NET Core 3.1) MVC專案。  

其中 Home 控制器程式碼如下:

using
Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace NETCoreMiddleware.Controllers { public class HomeController : Controller { private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger) { _logger = logger; } public IActionResult Index() { Console.WriteLine(""); Console.WriteLine($"This is {typeof(HomeController)} Index"); Console.WriteLine("");
return View(); } } }

其中 Startup.cs 類的程式碼如下:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace NETCoreMiddleware
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        //服務註冊(往容器中新增服務)
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
        }

        /// <summary>
        /// 配置Http請求處理管道
        /// Http請求管道模型---就是Http請求被處理的步驟
        /// 所謂管道,就是拿著HttpContext,經過多個步驟的加工,生成Response,這就是管道
        /// </summary>
        /// <param name="app"></param>
        /// <param name="env"></param>
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            #region 環境引數

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            #endregion 環境引數

            //靜態檔案中介軟體
            app.UseStaticFiles();

            #region Use中介軟體

            //中介軟體1
            app.Use(next =>
            {
                Console.WriteLine("middleware 1");
                return async context =>
                {
                    await Task.Run(() =>
                    {
                        Console.WriteLine("");
                        Console.WriteLine("===================================Middleware===================================");
                        Console.WriteLine($"This is middleware 1 Start");
                    });
                    await next.Invoke(context);
                    await Task.Run(() =>
                    {
                        Console.WriteLine($"This is middleware 1 End");
                        Console.WriteLine("===================================Middleware===================================");
                    });
                };
            });

            //中介軟體2
            app.Use(next =>
            {
                Console.WriteLine("middleware 2");
                return async context =>
                {
                    await Task.Run(() => Console.WriteLine($"This is middleware 2 Start"));
                    await next.Invoke(context); //可通過不呼叫 next 引數使請求管道短路
                    await Task.Run(() => Console.WriteLine($"This is middleware 2 End"));
                };
            });

            //中介軟體3
            app.Use(next =>
            {
                Console.WriteLine("middleware 3");
                return async context =>
                {
                    await Task.Run(() => Console.WriteLine($"This is middleware 3 Start"));
                    await next.Invoke(context);
                    await Task.Run(() => Console.WriteLine($"This is middleware 3 End"));
                };
            });

            #endregion Use中介軟體

            #region 最終把請求交給MVC

            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "areas",
                    pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });

            #endregion 最終把請求交給MVC
        }
    }
}

用 Use 將多個請求委託連結在一起,next 引數表示管道中的下一個委託(下一個中介軟體)。

下面我們使用命令列(CLI)方式啟動我們的網站,如下所示:

可以發現控制檯依次輸出了“middleware 3” 、“middleware 2”、“middleware 1”,這是怎麼回事呢?此處我們先留個疑問,該點在後面的講解中會再次提到。

啟動成功後,我們來訪問一下 “/home/index” ,控制檯輸出結果如下所示:

請求管道包含一系列請求委託,依次呼叫,下圖演示了這一過程:

每個委託均可在下一個委託前後執行操作。應儘早在管道中呼叫異常處理委託,這樣它們就能捕獲在管道的後期階段發生的異常。

此外,可通過不呼叫 next 引數使請求管道短路,如下所示:

/// <summary>
/// 配置Http請求處理管道
/// Http請求管道模型---就是Http請求被處理的步驟
/// 所謂管道,就是拿著HttpContext,經過多個步驟的加工,生成Response,這就是管道
/// </summary>
/// <param name="app"></param>
/// <param name="env"></param>
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    #region 環境引數

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    #endregion 環境引數

    //靜態檔案中介軟體
    app.UseStaticFiles();

    #region Use中介軟體

    //中介軟體1
    app.Use(next =>
    {
        Console.WriteLine("middleware 1");
        return async context =>
        {
            await Task.Run(() =>
            {
                Console.WriteLine("");
                Console.WriteLine("===================================Middleware===================================");
                Console.WriteLine($"This is middleware 1 Start");
            });
            await next.Invoke(context);
            await Task.Run(() =>
            {
                Console.WriteLine($"This is middleware 1 End");
                Console.WriteLine("===================================Middleware===================================");
            });
        };
    });

    //中介軟體2
    app.Use(next =>
    {
        Console.WriteLine("middleware 2");
        return async context =>
        {
            await Task.Run(() => Console.WriteLine($"This is middleware 2 Start"));
            //await next.Invoke(context); //可通過不呼叫 next 引數使請求管道短路
            await Task.Run(() => Console.WriteLine($"This is middleware 2 End"));
        };
    });

    //中介軟體3
    app.Use(next =>
    {
        Console.WriteLine("middleware 3");
        return async context =>
        {
            await Task.Run(() => Console.WriteLine($"This is middleware 3 Start"));
            await next.Invoke(context);
            await Task.Run(() => Console.WriteLine($"This is middleware 3 End"));
        };
    });

    #endregion Use中介軟體

    #region 最終把請求交給MVC

    app.UseRouting();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "areas",
            pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });

    #endregion 最終把請求交給MVC
}

此處我們註釋掉了 中介軟體2 的 next 引數呼叫,使請求管道短路。下面我們重新編譯後再次訪問 “/home/index” ,控制檯輸出結果如下所示:

當委託不將請求傳遞給下一個委託時,它被稱為“讓請求管道短路”。 通常需要短路,因為這樣可以避免不必要的工作。 例如,靜態檔案中介軟體可以處理對靜態檔案的請求,並讓管道的其餘部分短路,從而起到終端中介軟體的作用。 

對於終端中介軟體,框架專門為我們提供了一個叫 app.Run(...) 的擴充套件方法,其實該方法的內部也是呼叫 app.Use(...) 這個方法的,下面我們來看個示例:

/// <summary>
/// 配置Http請求處理管道
/// Http請求管道模型---就是Http請求被處理的步驟
/// 所謂管道,就是拿著HttpContext,經過多個步驟的加工,生成Response,這就是管道
/// </summary>
/// <param name="app"></param>
/// <param name="env"></param>
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    #region 環境引數

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    #endregion 環境引數

    //靜態檔案中介軟體
    app.UseStaticFiles();

    #region Use中介軟體

    //中介軟體1
    app.Use(next =>
    {
        Console.WriteLine("middleware 1");
        return async context =>
        {
            await Task.Run(() =>
            {
                Console.WriteLine("");
                Console.WriteLine("===================================Middleware===================================");
                Console.WriteLine($"This is middleware 1 Start");
            });
            await next.Invoke(context);
            await Task.Run(() =>
            {
                Console.WriteLine($"This is middleware 1 End");
                Console.WriteLine("===================================Middleware===================================");
            });
        };
    });

    //中介軟體2
    app.Use(next =>
    {
        Console.WriteLine("middleware 2");
        return async context =>
        {
            await Task.Run(() => Console.WriteLine($"This is middleware 2 Start"));
            await next.Invoke(context); //可通過不呼叫 next 引數使請求管道短路
            await Task.Run(() => Console.WriteLine($"This is middleware 2 End"));
        };
    });

    //中介軟體3
    app.Use(next =>
    {
        Console.WriteLine("middleware 3");
        return async context =>
        {
            await Task.Run(() => Console.WriteLine($"This is middleware 3 Start"));
            await next.Invoke(context);
            await Task.Run(() => Console.WriteLine($"This is middleware 3 End"));
        };
    });

    #endregion Use中介軟體

    #region 終端中介軟體

    //app.Use(_ => handler);
    app.Run(async context =>
    {
        await Task.Run(() => Console.WriteLine($"This is Run"));
    });

    #endregion 終端中介軟體

    #region 最終把請求交給MVC

    app.UseRouting();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "areas",
            pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });

    #endregion 最終把請求交給MVC
}

我們重新編譯後再次訪問 “/home/index” ,控制檯輸出結果如下所示:

Run 委託不會收到 next 引數。第一個 Run 委託始終為終端,用於終止管道。Run 是一種約定。某些中介軟體元件可能會公開在管道末尾執行 Run[Middleware] 方法。

此外,app.Use(...) 方法還有另外一個過載,如下所示(中介軟體4):

/// <summary>
/// 配置Http請求處理管道
/// Http請求管道模型---就是Http請求被處理的步驟
/// 所謂管道,就是拿著HttpContext,經過多個步驟的加工,生成Response,這就是管道
/// </summary>
/// <param name="app"></param>
/// <param name="env"></param>
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    #region 環境引數

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    #endregion 環境引數

    //靜態檔案中介軟體
    app.UseStaticFiles();

    #region Use中介軟體

    //中介軟體1
    app.Use(next =>
    {
        Console.WriteLine("middleware 1");
        return async context =>
        {
            await Task.Run(() =>
            {
                Console.WriteLine("");
                Console.WriteLine("===================================Middleware===================================");
                Console.WriteLine($"This is middleware 1 Start");
            });
            await next.Invoke(context);
            await Task.Run(() =>
            {
                Console.WriteLine($"This is middleware 1 End");
                Console.WriteLine("===================================Middleware===================================");
            });
        };
    });

    //中介軟體2
    app.Use(next =>
    {
        Console.WriteLine("middleware 2");
        return async context =>
        {
            await Task.Run(() => Console.WriteLine($"This is middleware 2 Start"));
            await next.Invoke(context); //可通過不呼叫 next 引數使請求管道短路
            await Task.Run(() => Console.WriteLine($"This is middleware 2 End"));
        };
    });

    //中介軟體3
    app.Use(next =>
    {
        Console.WriteLine("middleware 3");
        return async context =>
        {
            await Task.Run(() => Console.WriteLine($"This is middleware 3 Start"));
            await next.Invoke(context);
            await Task.Run(() => Console.WriteLine($"This is middleware 3 End"));
        };
    });

    //中介軟體4
    //Use方法的另外一個過載
    app.Use(async (context, next) =>
    {
        await Task.Run(() => Console.WriteLine($"This is middleware 4 Start"));
        await next();
        await Task.Run(() => Console.WriteLine($"This is middleware 4 End"));
    });

    #endregion Use中介軟體

    #region 終端中介軟體

    //app.Use(_ => handler);
    app.Run(async context =>
    {
        await Task.Run(() => Console.WriteLine($"This is Run"));
    });

    #endregion 終端中介軟體

    #region 最終把請求交給MVC

    app.UseRouting();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "areas",
            pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });

    #endregion 最終把請求交給MVC
}

我們重新編譯後再次訪問 “/home/index” ,控制檯輸出結果如下所示:

 

下面我們結合ASP.NET Core原始碼來分析下其實現原理: 

首先我們通過除錯來看下 IApplicationBuilder 的實現類到底是啥?如下所示:

可以看出它的實現類是  Microsoft.AspNetCore.Builder.ApplicationBuilder ,我們找到 ApplicationBuilder 類的原始碼,如下所示:

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Internal;

namespace Microsoft.AspNetCore.Builder
{
    public class ApplicationBuilder : IApplicationBuilder
    {
        private const string ServerFeaturesKey = "server.Features";
        private const string ApplicationServicesKey = "application.Services";

        private readonly IList<Func<RequestDelegate, RequestDelegate>> _components = new List<Func<RequestDelegate, RequestDelegate>>();

        public ApplicationBuilder(IServiceProvider serviceProvider)
        {
            Properties = new Dictionary<string, object>(StringComparer.Ordinal);
            ApplicationServices = serviceProvider;
        }

        public ApplicationBuilder(IServiceProvider serviceProvider, object server)
            : this(serviceProvider)
        {
            SetProperty(ServerFeaturesKey, server);
        }

        private ApplicationBuilder(ApplicationBuilder builder)
        {
            Properties = new CopyOnWriteDictionary<string, object>(builder.Properties, StringComparer.Ordinal);
        }

        public IServiceProvider ApplicationServices
        {
            get
            {
                return GetProperty<IServiceProvider>(ApplicationServicesKey);
            }
            set
            {
                SetProperty<IServiceProvider>(ApplicationServicesKey, value);
            }
        }

        public IFeatureCollection ServerFeatures
        {
            get
            {
                return GetProperty<IFeatureCollection>(ServerFeaturesKey);
            }
        }

        public IDictionary<string, object> Properties { get; }

        private T GetProperty<T>(string key)
        {
            object value;
            return Properties.TryGetValue(key, out value) ? (T)value : default(T);
        }

        private void SetProperty<T>(string key, T value)
        {
            Properties[key] = value;
        }

        public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
        {
            _components.Add(middleware);
            return this;
        }

        public IApplicationBuilder New()
        {
            return new ApplicationBuilder(this);
        }

        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;
        }
    }
}

其中 RequestDelegate 委託的宣告,如下:

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Threading.Tasks;

namespace Microsoft.AspNetCore.Http
{
    /// <summary>
    /// A function that can process an HTTP request.
    /// </summary>
    /// <param name="context">The <see cref="HttpContext"/> for the request.</param>
    /// <returns>A task that represents the completion of request processing.</returns>
    public delegate Task RequestDelegate(HttpContext context);
}

仔細閱讀後可以發現其實 app.Use(...) 這個方法就只是將 Func<RequestDelegate, RequestDelegate> 型別的委託引數新增到 _components 這個集合中。

最終程式會呼叫 ApplicationBuilder 類的 Build() 方法去構建Http請求處理管道,接下來我們就重點來關注一下這個 Build() 方法,如下:

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;
}

仔細觀察上面的原始碼後我們可以發現: 

1、首先它是將 _components 這個集合反轉(即:_components.Reverse()),然後依次呼叫裡面的中介軟體(Func<RequestDelegate, RequestDelegate>委託),這也就解釋了為什麼網站啟動時我們的控制檯會依次輸出 “middleware 3” 、“middleware 2”、“middleware 1” 的原因。 

2、呼叫反轉後的第一個中介軟體(即:註冊的最後一箇中間件)時傳入的引數是狀態碼為404的 RequestDelegate 委託,作為預設處理步驟。

3、在呼叫反轉後的中介軟體時,它是用第一個中介軟體的返回值作為呼叫第二個中介軟體的引數,用第二個中介軟體的返回值作為呼叫第三個中介軟體的引數,依次類推。這也就是為什麼說註冊時的那個 next 引數是指向註冊時下一個中介軟體的原因。 

4、Build() 方法最終返回的是呼叫反轉後最後一箇中間件(即:註冊的第一個中介軟體)的返回值。

下面我們來看一下Use方法的另外一個過載,如下所示:

//中介軟體4
//Use方法的另外一個過載
app.Use(async (context, next) =>
{
    await Task.Run(() => Console.WriteLine($"This is middleware 4 Start"));
    await next();
    await Task.Run(() => Console.WriteLine($"This is middleware 4 End"));
});

我們找到它的原始碼,如下:

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Builder
{
    /// <summary>
    /// Extension methods for adding middleware.
    /// </summary>
    public static class UseExtensions
    {
        /// <summary>
        /// Adds a middleware delegate defined in-line to the application's request pipeline.
        /// </summary>
        /// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param>
        /// <param name="middleware">A function that handles the request or calls the given next function.</param>
        /// <returns>The <see cref="IApplicationBuilder"/> instance.</returns>
        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);
                };
            });
        }
    }
}

可以發現其實它是個擴充套件方法,主要就是對 app.Use(...) 這個方法包裝了一下,最終呼叫的還是 app.Use(...) 這個方法。

最後我們來看一下 app.Run(...) 這個擴充套件方法,如下所示:

//app.Use(_ => handler);
app.Run(async context =>
{
    await Task.Run(() => Console.WriteLine($"This is Run"));
});

我們找到它的原始碼,如下:

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Builder
{
    /// <summary>
    /// Extension methods for adding terminal middleware.
    /// </summary>
    public static class RunExtensions
    {
        /// <summary>
        /// Adds a terminal middleware delegate to the application's request pipeline.
        /// </summary>
        /// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param>
        /// <param name="handler">A delegate that handles the request.</param>
        public static void Run(this IApplicationBuilder app, RequestDelegate handler)
        {
            if (app == null)
            {
                throw new ArgumentNullException(nameof(app));
            }

            if (handler == null)
            {
                throw new ArgumentNullException(nameof(handler));
            }

            app.Use(_ => handler);
        }
    }
}

可以發現,其實 app.Run(...) 這個擴充套件方法最終也是呼叫 app.Use(...) 這個方法,只不過它直接丟棄了 next 引數,故呼叫這個方法會終止管道,它屬於終端中介軟體。

更多關於ASP.NET Core 中介軟體相關知識可參考微軟官方文件: https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-6.0

至此本文就全部介紹完了,如果覺得對您有所啟發請記得點個贊哦!!!

 

Demo原始碼:

連結:https://pan.baidu.com/s/103ldhtjVcB3vJZidlcq0Yw 
提取碼:7rt1

此文由博主精心撰寫轉載請保留此原文連結https://www.cnblogs.com/xyh9039/p/16146620.html

版權宣告:如有雷同純屬巧合,如有侵權請及時聯絡本人修改,謝謝!!!