1. 程式人生 > >ASP.NET Core路由中介軟體[4]: EndpointRoutingMiddleware和EndpointMiddleware

ASP.NET Core路由中介軟體[4]: EndpointRoutingMiddleware和EndpointMiddleware

針對終結點的路由是由EndpointRoutingMiddleware和EndpointMiddleware這兩個中介軟體協同完成的。應用在啟動之前會註冊若干表示終結點的Endpoint物件(具體來說是包含路由模式的RouteEndpoint物件)。如下圖所示,當應用接收到請求並建立HttpContext上下文之後,EndpointRoutingMiddleware中介軟體會根據請求的URL及其他相關資訊從註冊的終結點中選擇匹配度最高的那個。之後被選擇的終結點會以一個特性(Feature)的形式附加到當前HttpContext上下文中,EndpointMiddleware中介軟體最終提供這個終結點並用它來處理當前請求。[更多關於ASP.NET Core的文章請點這裡]

目錄
一、IEndpointFeature
二、EndpointRoutingMiddleware
三、EndpointMiddleware
四、註冊終結點

一、IEndpointFeature

EndpointRoutingMiddleware中介軟體選擇的終結點會以特性的形式存放在當前HttpContext上下文中,這個用來封裝終結點的特性通過IEndpointFeature介面表示。如下面的程式碼片段所示,IEndpointFeature介面通過唯一的屬性Endpoint表示針對當前請求選擇的終結點。我們可以呼叫HttpContext型別的GetEndpoint方法和SetEndpoint方法來獲取與設定用來處理當前請求的終結點。

public interface IEndpointFeature
{
    Endpoint Endpoint { get; set; }
}

public static class EndpointHttpContextExtensions
{
    public static Endpoint GetEndpoint(this HttpContext context)  =>context.Features.Get<IEndpointFeature>()?.Endpoint;

    public static void SetEndpoint(this HttpContext context, Endpoint endpoint)
    {
        var  feature = context.Features.Get<IEndpointFeature>();
        if (feature != null)
        {
            feature.Endpoint = endpoint;
        }
        else
        {
            context.Features.Set<IEndpointFeature>(new EndpointFeature { Endpoint = endpoint });
        }       
    }
    private class EndpointFeature : IEndpointFeature
    {
        public Endpoint Endpoint { get; set; }
    }
}

二、EndpointRoutingMiddleware

EndpointRoutingMiddleware中介軟體利用一個Matcher物件選擇出與當前HttpContext上下文相匹配的終結點,然後將選擇的終結點以IEndpointFeature特性的形式附加到當前HttpContext上下文中。Matcher只是一個內部抽象型別,針對終結點的選擇和設定實現在它的MatchAsync方法中。如果匹配的終結點被成功選擇出來,MatchAsync方法還會提取出解析出來的路由引數,然後將它們逐個新增到表示當前請求的HttpRequest物件的RouteValues屬性字典中。

internal abstract class Matcher
{
    public abstract Task MatchAsync(HttpContext httpContext);
}

public abstract class HttpRequest
{
    public virtual RouteValueDictionary RouteValues { get; set; }
}

public class RouteValueDictionary : IDictionary<string, object>, IReadOnlyDictionary<string, object>
{
  ...  
}

EndpointRoutingMiddleware中介軟體使用的Matcher由註冊的MatcherFactory服務來提供。路由系統預設使用的Matcher型別為DfaMatcher,它採用一種被稱為確定有限狀態自動機(Deterministic Finite Automaton,DFA)的形式從候選終結點中找到與當前請求匹配度最高的那個。由於篇幅有限,具體的細節此處不再展開介紹。DfaMatcher最終會利用DfaMatcherFactory物件間接地創建出來,DfaMatcherFactory型別派生於抽象類MatcherFactory。

internal abstract class MatcherFactory
{
    public abstract Matcher CreateMatcher(EndpointDataSource dataSource);
}

對Matcher和MatcherFactory有了基本瞭解之後,我們將關注點轉移到EndpointRoutingMiddleware中介軟體。如下所示的程式碼片段模擬了EndpointRoutingMiddleware中介軟體的實現邏輯。我們在建構函式中注入了用於提供註冊終結點的IEndpointRouteBuilder物件和用來建立Matcher物件的MatcherFactory工廠。

internal class EndpointRoutingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly Task<Matcher> _matcherAccessor;

    public EndpointRoutingMiddleware(RequestDelegate next, IEndpointRouteBuilder builder, MatcherFactory factory)
    {
        _next = next;
        _matcherAccessor = new Task<Matcher>(CreateMatcher);

        Matcher CreateMatcher()
        {
            var source = new CompositeEndpointDataSource(builder.DataSources);
            return factory.CreateMatcher(source);
        }
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        var matcher = await _matcherAccessor;
        await matcher.MatchAsync(httpContext);
        await _next(httpContext);
    }
}

在實現的InvokeAsync方法中,我們只需要根據IEndpointRouteBuilder物件提供的終結點列表建立一個CompositeEndpointDataSource物件,並將其作為引數呼叫MatcherFactory工廠的CreateMatcher方法。該方法會返回一個Matcher物件,然後呼叫Matcher物件的MatchAsync方法選擇出匹配的終結點,並以特性的方式附加到當前HttpContext上下文中。EndpointRoutingMiddleware中介軟體一般通過如下所示的UseRouting擴充套件方法進行註冊。

public static class EndpointRoutingApplicationBuilderExtensions
{
    public static IApplicationBuilder UseRouting(this IApplicationBuilder builder);
}

三、EndpointMiddleware

EndpointMiddleware中介軟體的職責特別明確,就是執行由EndpointRoutingMiddleware中介軟體附加到當前HttpContext上下文中的終結點。EndpointRoutingMiddleware中介軟體針對終結點的執行涉及如下所示的RouteOptions型別標識的配置選項。

public class RouteOptions
{
    public bool LowercaseUrls { get; set; }
    public bool LowercaseQueryStrings { get; set; }
    public bool AppendTrailingSlash { get; set; }
    public IDictionary<string, Type> ConstraintMap { get; set; }

    public bool SuppressCheckForUnhandledSecurityMetadata { get; set; }
}

配置選項RouteOptions的前三個屬性與路由系統針對URL的生成有關。具體來說,LowercaseUrls屬性和LowercaseQueryStrings屬性決定是否會將生成的URL或者查詢字串轉換成小寫形式。AppendTrailingSlash屬性則決定是否會為生成的URL新增字尾“/”。RouteOptions的ConstraintMap屬性表示的字典與路由引數的內聯約束有關,它提供了在路由模板中實現的約束字串(如regex表示正則表示式約束)與對應約束型別(正則表示式約束型別為RegexRouteConstraint)之間的對映關係。

真正與EndpointMiddleware中介軟體相關的是RouteOptions的SuppressCheckForUnhandledSecurityMetadata屬性,它表示目標終結點利用新增的元資料設定了一些關於安全方面的要求(主要是授權和跨域資源共享方面的要求),但是目前的請求並未經過相應的中介軟體處理(通過請求是否具有要求的報頭判斷),在這種情況下是否還有必要繼續執行目標終結點。如果這個屬性設定為True,就意味著EndpointMiddleware中介軟體根本不會做這方面的檢驗。如下所示的程式碼片段模擬了EndpointMiddleware中介軟體對請求的處理邏輯。

internal class EndpointMiddleware
{
    private readonly RequestDelegate _next;
    private readonly RouteOptions _options;

    public EndpointMiddleware(RequestDelegate next, IOptions<RouteOptions> optionsAccessor)
    {
        _next = next;
        _options = optionsAccessor.Value;
    }

    public Task InvokeAsync(HttpContext httpContext)
    {
        var endpoint = httpContext.GetEndpoint();
        if (null != endpoint)
        {
            if (!_options.SuppressCheckForUnhandledSecurityMetadata)
            {
                CheckSecurity();
            }
            return endpoint.RequestDelegate(httpContext);
        }
        return _next(httpContext);
    }

    private void CheckSecurity();
}

我們一般呼叫如下所示的UseEndpoints擴充套件方法來註冊EndpointMiddleware中介軟體,該方法提供了一個型別為Action<IEndpointRouteBuilder>的引數。通過前面的介紹可知,EndpointRoutingMiddleware中介軟體會利用注入的IEndpointRouteBuilder物件來獲取註冊的表示終結點資料來源的EndpointDataSource,所以可以通過這個方法為EndpointRoutingMiddleware中介軟體註冊終結點資料來源。

public static class EndpointRoutingApplicationBuilderExtensions
{    
    public static IApplicationBuilder UseEndpoints(this IApplicationBuilder builder, Action<IEndpointRouteBuilder> configure);
}

四、註冊終結點

對於使用路由系統的應用程式來說,它的主要工作基本集中在針對EndpointDataSource的註冊上。一般來說,當我們呼叫IApplicationBuilder介面的UseEndpoints擴充套件方法註冊EndpointMiddleware中介軟體時,會利用提供的Action<IEndpointRouteBuilder>委託物件註冊所需的EndpointDataSource物件。IEndpointRouteBuilder介面具有一系列的擴充套件方法,這些方法可以幫助我們註冊所需的終結點。

如下所示的Map方法會根據提供的作為路由模式和處理器的RoutePattern物件與RequestDelegate物件建立一個終結點,並以ModelEndpointDataSource的形式予以註冊。如下所示的程式碼片段還揭示了一個細節:對於作為請求處理器的RequestDelegate委託物件來說,其對應方法上標註的所有特性會以元資料的形式新增到建立的終結點上。

public static class EndpointRouteBuilderExtensions
{
    public static IEndpointConventionBuilder Map(this IEndpointRouteBuilder endpoints, RoutePattern pattern, RequestDelegate requestDelegate)
    {
        var builder = new RouteEndpointBuilder(requestDelegate, pattern, 0)
        {
            DisplayName = pattern.RawText
        };
        var attributes = requestDelegate.Method.GetCustomAttributes();

        if (attributes != null)
        {
            foreach (var attribute in attributes)
            {
                builder.Metadata.Add(attribute);
            }
        }
        var dataSource = endpoints.DataSources.OfType<ModelEndpointDataSource>().FirstOrDefault()?? new ModelEndpointDataSource();
        endpoints.DataSources.Add(dataSource);
        return dataSource.AddEndpointBuilder(builder);
    }
}

HTTP方法(Method)在RESTful API的設計中具有重要意義,幾乎所有的終結點都會根據自身對資源的操作型別對請求採用HTTP方法做相應限制。如果需要為註冊的終結點指定限定的HTTP方法,就可以呼叫如下所示的MapMethods方法。該方法會在Map方法的基礎上為註冊的終結點設定相應的顯示名稱,並針對指定的HTTP方法建立一個HttpMethodMetadata物件,然後作為元資料新增到註冊的終結點上。

public static class EndpointRouteBuilderExtensions
{
    public static IEndpointConventionBuilder MapMethods(this IEndpointRouteBuilder endpoints, string pattern, IEnumerable<string> httpMethods, RequestDelegate requestDelegate)
    {       
        var builder = endpoints.Map(RoutePatternFactory.Parse(pattern), requestDelegate);
        builder.WithDisplayName($"{pattern} HTTP: {string.Join(", ", httpMethods)}");
        builder.WithMetadata(new HttpMethodMetadata(httpMethods));
        return builder;
    }
}

EndpointRoutingMiddleware中介軟體在為當前請求篩選匹配的終結點時,針對HTTP方法的選擇策略是通過IHttpMethodMetadata介面表示的元資料指定的,HttpMethodMetadata型別正是對該介面的預設實現。如下面的程式碼片段所示,IHttpMethodMetadata介面除了具有一個表示可接受HTTP方法列表的HttpMethods屬性,還有一個布林型別的只讀屬性AcceptCorsPreflight,它表示是否接受針對跨域資源共享(Cross-Origin Resource Sharing,CORS)的預檢(Preflight)請求。

public interface IHttpMethodMetadata
{
    IReadOnlyList<string> HttpMethods { get; }
    bool AcceptCorsPreflight { get; }
}

public sealed class HttpMethodMetadata : IHttpMethodMetadata
{        
    public IReadOnlyList<string> HttpMethods { get; }
    public bool AcceptCorsPreflight { get; }  

    public HttpMethodMetadata(IEnumerable<string> httpMethods): this(httpMethods, acceptCorsPreflight: false)
    {}
   
    public HttpMethodMetadata(IEnumerable<string> httpMethods, bool acceptCorsPreflight)
    {
        HttpMethods = httpMethods.ToArray();
        AcceptCorsPreflight = acceptCorsPreflight;
    }   
}

路由系統還為4種常用的HTTP方法(GET、POST、PUT和DELETE)定義了相應的方法。從如下所示的程式碼片段可以看出,它們最終呼叫的都是MapMethods方法。我們在本章開篇演示的例項中正是呼叫其中的MapGet方法來註冊終結點的。

public static class EndpointRouteBuilderExtensions
{
    public static IEndpointConventionBuilder MapGet(this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate)
        => MapMethods(endpoints, pattern, "GET", requestDelegate);
    public static IEndpointConventionBuilder MapPost(this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate)
        => MapMethods(endpoints, pattern, "POST", requestDelegate);
    public static IEndpointConventionBuilder MapPut(this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate)
        => MapMethods(endpoints, pattern, "PUT", requestDelegate);
    public static IEndpointConventionBuilder MapDelete(this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate)
        => MapMethods(endpoints, pattern, "DELETE", requestDelegate);
}

呼叫IApplicationBuilder介面相應的擴充套件方法註冊EndpointRoutingMiddleware中介軟體和EndpointMiddleware中介軟體時,必須確保它們依賴的服務已經被註冊到依賴注入框架之中。針對路由服務的註冊可以通過呼叫如下所示的AddRouting擴充套件方法過載來完成。

public static class RoutingServiceCollectionExtensions
{
    public static IServiceCollection AddRouting(this IServiceCollection services);
    public static IServiceCollection AddRouting(this IServiceCollection services, Action<RouteOptions> configureOptions);
}

ASP.NET Core路由中介軟體[1]: 終結點與URL的對映
ASP.NET Core路由中介軟體[2]: 路由模式
ASP.NET Core路由中介軟體[3]: 終結點
ASP.NET Core路由中介軟體[4]: EndpointRoutingMiddleware和EndpointMiddleware
ASP.NET Core路由中介軟體[5]: 路由約束