換個角度學習ASP.NET Core中介軟體

Liuww06發表於2020-05-23

中介軟體真面目

關於ASP.NET Core中介軟體是啥,簡單一句話描述就是:用來處理HTTP請求和響應的一段邏輯,並且可以決定是否把請求傳遞到管道中的下一個中介軟體!

上面只是概念上的一種文字描述,那問題來了,中介軟體在程式中到底是個啥❓

一切還是從IApplicationBuilder說起,沒錯,就是大家熟悉的Startup類裡面那個Configure方法裡面的那個IApplicationBuilder(有點繞?,抓住重點就行)。

IApplicationBuilder,應用構建者,聽這個名字就能感受它的核心地位,ASP.NET Core應用就是依賴它構建出來,看看它的定義:

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

Use方法用來把中介軟體新增到應用管道中,此時我們已經看到中介軟體的真面目了,原來是一個委託,輸入引數是RequestDelegate,返回也是RequestDelegate,其實RequestDelegate還是個委託,如下:

public delegate Task RequestDelegate(HttpContext context);

還記得中介軟體是幹嘛的嗎?是用來處理http請求和響應的,即對HttpContext的處理,這裡我們可以看出來原來中介軟體的業務邏輯就是封裝在RequestDelegate裡面。

總結一下:

middleware就是Func<RequestDelegate, RequestDelegate>,輸入的是下一個中介軟體的業務處理邏輯,返回的就是當前中介軟體的業務處理邏輯,並在其中決定要不要呼叫下箇中介軟體!我們程式碼實現一箇中介軟體看看(可能和我們平時用的不太一樣,但它就是中介軟體最原始的形式!):

//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);
 

跑一下瞅瞅,成功執行中介軟體!

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

中介軟體管道

通過上面我們有沒有注意到,新增中間時,他們都是一個一個獨立的被新增進去,而中介軟體管道就是負責把中介軟體串聯起來,實現下面的一箇中介軟體呼叫流轉流程:

如何實現呢?這個就是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

結語

中介軟體存在這麼多的使用方式,每一個存在都是為了解決實際需求的,當我們瞭解這些背景知識後,在後面自己使用時,就能更加的靈活!

相關文章