原始碼解析.Net中Middleware的實現

SnailZz發表於2021-09-03

前言

本篇繼續之前的思路,不注重用法,如果還不知道有哪些用法的小夥伴,可以點選這裡,微軟文件說的很詳細,在閱讀本篇文章前,還是希望你對中介軟體有大致的瞭解,這樣你讀起來可能更加能夠意會到意思。廢話不多說,我們們進入正題(ps:讀者要注意關注原始碼的註釋哦?)。

Middleware類之間的關係

下圖也是隻列出重要的類和方法,其主要就是就ApplicationBuilder類,如下圖:

原始碼解析

1.在使用中介軟體時,需要在StartUp類的Config方法中來完成(.Net自帶的中介軟體,官方有明確過使用的順序,可以看文件),例如Use,Map,Run等方法,它們都通過IApplicationBuilder內建函式呼叫,所以我們先看ApplicationBuilder類的主體構造,程式碼如下圖:

//這個是所有中介軟體的委託
public delegate Task RequestDelegate(HttpContext context);
public class ApplicationBuilder : IApplicationBuilder
{
    //服務特性集合key
    private const string ServerFeaturesKey = "server.Features";
    //注入的服務集合key
    private const string ApplicationServicesKey = "application.Services";
    //新增的中介軟體集合
    private readonly List<Func<RequestDelegate, RequestDelegate>> _components = new();
    
    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)
    {
        return Properties.TryGetValue(key, out var 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;
    }
    //建立新的AppBuilder
    public IApplicationBuilder New()
    {
        return new ApplicationBuilder(this);
    }
    //執行Build,構造委託鏈
    public RequestDelegate Build()
    {
        RequestDelegate app = context =>
        {
            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 = StatusCodes.Status404NotFound;
            return Task.CompletedTask;
        };
        //後新增的在末端,先新增的先執行
        for (var c = _components.Count - 1; c >= 0; c--)
        {
            app = _components[c](app);
        }

        return app;
    }
}

根據上述程式碼可以看出,向集合中新增項只能呼叫Use方法,然後在Build方法時將委託全部構造成鏈,請求引數是HttpContext,也就是說,每次請求時,直接呼叫這個鏈路頭部的委託就可以把所有方法走一遍。

  1. 接下來,看一下那些自定義的中介軟體是怎麼加入到管道,並且在.net中是怎麼處理的,原始碼如下:
public static class UseMiddlewareExtensions
{
    internal const string InvokeMethodName = "Invoke";
    internal const string InvokeAsyncMethodName = "InvokeAsync";

    public static IApplicationBuilder UseMiddleware<[DynamicallyAccessedMembers(MiddlewareAccessibility)]TMiddleware>(this IApplicationBuilder app, params object?[] args)
    {
        return app.UseMiddleware(typeof(TMiddleware), args);
    }

    public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, [DynamicallyAccessedMembers(MiddlewareAccessibility)] Type middleware, params object?[] args)
    {    
        //判斷如果是以依賴注入的形式加入的中介軟體,需要繼承IMiddleware,則不允許有引數
        if (typeof(IMiddleware).IsAssignableFrom(middleware))
        {
            if (args.Length > 0)
            {
                throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)));
            }

            return UseMiddlewareInterface(app, middleware);
        }

        var applicationServices = app.ApplicationServices;
        return app.Use(next =>
        {  
            //檢查是否有Invoke或者InvokeAsync方法
            var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public);
            var invokeMethods = methods.Where(m =>
                string.Equals(m.Name, InvokeMethodName, StringComparison.Ordinal)
                || string.Equals(m.Name, InvokeAsyncMethodName, StringComparison.Ordinal)
                ).ToArray();
            //且不能超過一個
            if (invokeMethods.Length > 1)
            {
                throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName, InvokeAsyncMethodName));
            }
            //也不能等於零個
            if (invokeMethods.Length == 0)
            {
                throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName, InvokeAsyncMethodName, middleware));
            }
            
            var methodInfo = invokeMethods[0];
            //返回型別必須是Task
            if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType))
            {
                throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task)));
            }
            //獲取Invoke方法引數
            var parameters = methodInfo.GetParameters();
            if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext))
            {
                throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext)));
            }
            //第一個引數是RequestDelegate
            var ctorArgs = new object[args.Length + 1];
            ctorArgs[0] = next;
            Array.Copy(args, 0, ctorArgs, 1, args.Length);
            //根據建構函式引數建立例項
            var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs);
            if (parameters.Length == 1)
            {   
                //如果是隻有一個引數,直接根據例項Invoke方法,建立RequestDelegate委託
                return (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate), instance);
            }
            
            //說明Invoke有容器注入的其他服務,則這個方法就是獲取那些服務
            var factory = Compile<object>(methodInfo, parameters);

            return context =>
            {  
                //預設是請求的Scope容器,如果是null,則返回根容器
                var serviceProvider = context.RequestServices ?? applicationServices;
                if (serviceProvider == null)
                {
                    throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));
                }
                //執行Invoke方法
                return factory(instance, context, serviceProvider);
            };
        });
    }

    private static IApplicationBuilder UseMiddlewareInterface(IApplicationBuilder app, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type middlewareType)
    {
        //呼叫Use方法,將委託新增到ApplicationBuilder的記憶體集合裡
        return app.Use(next =>
        {
            return async context =>
            {
                //獲取中介軟體工廠類,從Scope容器中獲取注入的中介軟體
                var middlewareFactory = (IMiddlewareFactory?)context.RequestServices.GetService(typeof(IMiddlewareFactory));
                if (middlewareFactory == null)
                {
                    throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoMiddlewareFactory(typeof(IMiddlewareFactory)));
                }
                //獲取中介軟體注入的物件例項
                var middleware = middlewareFactory.Create(middlewareType);
                if (middleware == null)
                {
                    throw new InvalidOperationException(Resources.FormatException_UseMiddlewareUnableToCreateMiddleware(middlewareFactory.GetType(), middlewareType));
                }

                try
                { 
                    //呼叫InvokeAsync方法
                    await middleware.InvokeAsync(context, next);
                }
                finally
                {
                    //實際上沒做處理,和容器的生命週期一致
                    middlewareFactory.Release(middleware);
                }
            };
        });
    }
}

根據上面的程式碼可以看出,根據不同方式注入的中介軟體,.Net做了不同的處理,並且對自定義的中介軟體做型別檢查,但是最後必須呼叫app.Use方法,將委託加入到ApplicationBuilder的記憶體集合裡面,到Build階段處理。

  1. 上面介紹了自定義中介軟體的處理方式,接下里我們依次介紹下Use,Map和Run方法的處理,原始碼如下:
public static class UseExtensions
{
    public static IApplicationBuilder Use(this IApplicationBuilder app, Func<HttpContext, Func<Task>, Task> middleware)
    {
        //呼叫Use方法,新增到記憶體集合裡
        return app.Use(next =>
        {
            return context =>
            {
                //next就是下一個處理,也就是RequestDelegate
                Func<Task> simpleNext = () => next(context);
                return middleware(context, simpleNext);
            };
        });
    }
}
public static class MapExtensions
{
    public static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, Action<IApplicationBuilder> configuration)
    {
        return Map(app, pathMatch, preserveMatchedPathSegment: false, configuration);
    }
    public static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, bool preserveMatchedPathSegment, Action<IApplicationBuilder> configuration)
    {
        if (app == null)
        {
            throw new ArgumentNullException(nameof(app));
        }

        if (configuration == null)
        {
            throw new ArgumentNullException(nameof(configuration));
        }
        //不能是/結尾,這個!.用法我也是學習到了
        if (pathMatch.HasValue && pathMatch.Value!.EndsWith("/", StringComparison.Ordinal))
        {
            throw new ArgumentException("The path must not end with a '/'", nameof(pathMatch));
        }
        //構建新的ApplicationBuilder物件,裡面不包含之前新增的中介軟體
        var branchBuilder = app.New();
        //新分支裡面的中介軟體
        configuration(branchBuilder);
        //執行Build方法構建分支管道
        var branch = branchBuilder.Build();

        var options = new MapOptions
        {
            Branch = branch,
            PathMatch = pathMatch,
            PreserveMatchedPathSegment = preserveMatchedPathSegment
        };
        //內部其實是檢查是否匹配,匹配的話執行Branch,不匹配繼續執行next
        return app.Use(next => new MapMiddleware(next, options).Invoke);
    }
}
public static class RunExtensions
{
    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));
        }
        //只執行Handle,不做其他處理,也就是管道終端,給短路了
        app.Use(_ => handler);
    }
}

上面的程式碼分別介紹了Use,Map,Run的方法實現,它們還是在需要將中介軟體加入到記憶體集合裡面,但是對於不同的方法,它們達到的效果也不一樣。

  1. 總結上面的程式碼可以看出,它執行完Build方法,把委託鏈構造出來之後,然後在每次請求的時候只需要將構造完成的HttpContext當作請求引數傳入之後,即可依次執行中介軟體的內容,那麼應用程式是如何構建ApplicationBuilder物件例項,又是在哪裡呼叫Build方法的呢?我們繼續往下看。
    我們知道在使用.Net通用模板的建立專案的時候,在Program裡面有一句程式碼,如下:
Host.CreateDefaultBuilder(args)
//這個方法主要是構建web主機,如Kestrel,整合IIS等操作
.ConfigureWebHostDefaults(webBuilder =>
{
    webBuilder.UseStartup<Startup>();
});

追溯其原始碼的位置時,實際上它是作為了IHostedService服務來執行的,如果有不清楚IHostedService的小夥伴可以點選這裡,先看下官方文件的解釋和用法,看完之後你就明白了。我們再來看原始碼:

public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<IWebHostBuilder> configure, Action<WebHostBuilderOptions> configureWebHostBuilder)
{
    if (configure is null)
    {
        throw new ArgumentNullException(nameof(configure));
    }

    if (configureWebHostBuilder is null)
    {
        throw new ArgumentNullException(nameof(configureWebHostBuilder));
    }

    if (builder is ISupportsConfigureWebHost supportsConfigureWebHost)
    {
        return supportsConfigureWebHost.ConfigureWebHost(configure, configureWebHostBuilder);
    }

    var webHostBuilderOptions = new WebHostBuilderOptions();
    //下面兩行執行的程式碼,是關於構建Host主機的,以後新的文章來說
    configureWebHostBuilder(webHostBuilderOptions);
    var webhostBuilder = new GenericWebHostBuilder(builder, webHostBuilderOptions);
    //執行自定的方法,例如模板方法裡面的UseStartUp
    configure(webhostBuilder);
    //主要看這裡,將其新增到HostedService
    builder.ConfigureServices((context, services) => services.AddHostedService<GenericWebHostService>());
    return builder;
}
internal sealed partial class GenericWebHostService : IHostedService
{
  public async Task StartAsync(CancellationToken cancellationToken)
  {
      HostingEventSource.Log.HostStart();

      var serverAddressesFeature = Server.Features.Get<IServerAddressesFeature>();
      var addresses = serverAddressesFeature?.Addresses;
      //配置服務地址
      if (addresses != null && !addresses.IsReadOnly && addresses.Count == 0)
      {
          var urls = Configuration[WebHostDefaults.ServerUrlsKey];
          if (!string.IsNullOrEmpty(urls))
          {
              serverAddressesFeature!.PreferHostingUrls = WebHostUtilities.ParseBool(Configuration, WebHostDefaults.PreferHostingUrlsKey);

              foreach (var value in urls.Split(';', StringSplitOptions.RemoveEmptyEntries))
              {
                  addresses.Add(value);
              }
          }
      }
      //定義最終返回的委託變數
      RequestDelegate? application = null;

      try
      {  
          //預設StartUp類裡面的Config
          var configure = Options.ConfigureApplication;

          if (configure == null)
          {
              throw new InvalidOperationException($"No application configured. Please specify an application via IWebHostBuilder.UseStartup, IWebHostBuilder.Configure, or specifying the startup assembly via {nameof(WebHostDefaults.StartupAssemblyKey)} in the web host configuration.");
          }
          //構建ApplicationBuilder
          var builder = ApplicationBuilderFactory.CreateBuilder(Server.Features);
          //如果存在IStartupFilter,那麼把要執行的中介軟體放到前面
          foreach (var filter in StartupFilters.Reverse())
          {
              configure = filter.Configure(configure);
          }
          configure(builder);
          //執行Build,開始構建委託鏈
          application = builder.Build();
      }
      catch (Exception ex)
      {
          Logger.ApplicationError(ex);

          if (!Options.WebHostOptions.CaptureStartupErrors)
          {
              throw;
          }

          var showDetailedErrors = HostingEnvironment.IsDevelopment() || Options.WebHostOptions.DetailedErrors;

          application = ErrorPageBuilder.BuildErrorPageApplication(HostingEnvironment.ContentRootFileProvider, Logger, showDetailedErrors, ex);
      }

      var httpApplication = new HostingApplication(application, Logger, DiagnosticListener, ActivitySource, Propagator, HttpContextFactory);
      //啟動服務
      await Server.StartAsync(httpApplication, cancellationToken);
  }
}

從上面可以看出,最終是作為IHostedService來執行的,而StartAsync方法,則是在Host.Build().Run()中的Run方法裡面統一執行所有註冊過IHostedService服務的集合,也就是在Run階段才開始構建管道(讀者可以自行看下原始碼,以後的文章我也會講到)。

總結

通過解讀原始碼可以看出中介軟體有以下特點:

  • 目前自定義的中介軟體要麼需要繼承IMiddleware(不能傳遞引數),要麼需要構造指定規則的類。
  • Use不會使管道短路(除非呼叫方不呼叫next),Map和Run會使管道短路,更多的是,Run不會再往下傳遞,也就是終止,而Map可能會往下傳遞。
  • 委託鏈的構造是在Run方法中執行的,並且作為IHostedService託管服務執行的。

上述文章中所展示的原始碼並不是全部的原始碼,筆者只是挑出其重點部分展示。由於文采能力有限?,如果有沒說明白的或者沒有描述清楚的,又或者有錯誤的地方,還請評論指正。

相關文章