1. 程式人生 > >ASP.NET Core路由中介軟體[2]: 路由模式

ASP.NET Core路由中介軟體[2]: 路由模式

一個Web應用本質上體現為一組終結點的集合。終結點則體現為一個暴露在網路中可供外界採用HTTP協議呼叫的服務,路由的作用就是建立一個請求URL模式與對應終結點之間的對映關係。藉助這個對映關係,客戶端可以採用模式匹配的URL來呼叫對應的終結點。除了利用下圖所示的對映關係對請求進行路由解析,然後選擇並執行與之匹配的終結點,路由系統還可以註冊路由的URL模式和指定的路由引數值生成一個完整的URL。我們將這兩方面的工作稱為兩個路由方向(Routing Direction),前者為入棧路由(Inbound Routing),後者為出棧路由(Outbound Routing)。[更多關於ASP.NET Core的文章請點這裡]

對於路由系統來說,作為路由目標的終結點總是關聯一個具體的URL路徑模式,我們將其稱為路由模式(Route Pattern)。表示路由模式的RoutePattern是通過解析路由註冊時提供的路由模板生成的,路由模式的基本組成元素通過抽象型別RoutePatternPart表示。

一、RoutePatternPart

RoutePatternPart在路由模板中主要有兩種型別:一種是靜態文字,另一種是路由引數。例如,包含兩段的路由模板“foo/{bar}”,第一段為靜態文字,第二段為路由引數。由於花括號在路由模板中被用來定義路由引數,如果靜態文字中包含“{”和“}”字元,就需要採用“{{”和“}}”進行轉義。

其實除了上述這兩種基本型別,RoutePatternPart還有第三種類型。例如,如果採用字串“files/{name}.{ext?}”來表示針對某個檔案的路由模板,檔名({name})和副檔名(ext?)體現為路由引數,而它們之間的“.”就是RoutePattern的第三種展現形式,被稱為分隔符。路由系統對於分隔符具有特殊的匹配邏輯:如果分隔符後面跟的是一個可以預設的路由引數,請求地址在沒有提供該引數值的情況下,分隔符是可以預設的。對於“files/{name}.{ext?}”這個路由模板來說,副檔名是可以預設的,如果請求地址沒有提供副檔名,請求路徑只需要提供檔名(如/files/foobar)即可。RoutePatternPart的3種類型通過RoutePatternPartKind列舉表示。

public enum RoutePatternPartKind
{
    Literal,
    Parameter,
    Separator
}

如下所示的程式碼片段是RoutePatternPart的定義,可以看出這是一個抽象類。除了定義表示型別的PartKind只讀屬性,RoutePatternPart還有3個布林型別的屬性(IsLiteral、IsParameter和IsSeparator),它們表示當前是否屬於對應的型別。

public abstract class RoutePatternPart
{
    public RoutePatternPartKind PartKind { get; }

    public bool IsLiteral { get; }
    public bool IsParameter { get; }
    public bool IsSeparator { get; }
}

針對RoutePatternPartKind列舉體現的3種類型,路由系統提供3個針對RoutePatternPart的派生類,如下所示的程式碼片段是針對靜態文字和分隔符的RoutePatternLiteralPart與RoutePattern
SeparatorPart型別的定義,它們具有表示具體內容(靜態文字內容和分隔符)的Content屬性。

public sealed class RoutePatternLiteralPart : RoutePatternPart
{
    public string Content { get; }
}

public sealed class RoutePatternSeparatorPart : RoutePatternPart
{
    public string Content { get; }
}

由於路由引數在路由模板中有多種定義形式,所以對應的RoutePatternParameterPart型別的成員會多一些。RoutePatternParameterPart的Name屬性和ParameterKind屬性表示路由引數的名稱與型別。路由引數型別包括標準形式(如{foobar})、預設形式(如{foobar?}或者{foobar?=123})及萬用字元形式(如{*foobar}或者{**foobar})。路由引數的這3種定義形式通過RoutePatternParameterKind列舉表示。

public sealed class RoutePatternParameterPart : RoutePatternPart
{
    public string Name { get; }
    public RoutePatternParameterKind ParameterKind { get; }
    public bool IsOptional { get; }
    public object Default { get; }
    public bool IsCatchAll { get; }
    public bool EncodeSlashes { get; }

    public IReadOnlyList<RoutePatternParameterPolicyReference> ParameterPolicies { get; }
}

public enum RoutePatternParameterKind
{
    Standard,
    Optional,
    CatchAll
}

對於預設形式或者萬用字元形式對應的路由引數,對應RoutePatternParameterPart物件的IsOptional屬性和IsCatchAll屬性會返回True。如果為引數定義了預設值,該值體現在Default屬性上。對於兩種萬用字元形式定義的路由引數,針對請求URL的解析來說並沒有什麼不同,它們之間的差異體現在路由系統根據它生成對應URL的時候。具體來說,對於提供的包含分隔符“/”的引數值(如foo/bar),如果對應的路由引數採用{*variable}的方式,URL格式化過程中會對分隔符進行編碼(foo%2bar),倘若路由引數採用{**variable}的形式定義,提供的字串將不做任何改變。RoutePatternParameterPart的EncodeSlashes屬性表示是否需要對路徑分隔符“/”進行編碼。

我們在定義路由引數時可以指定約束條件,路由系統將約束視為一種引數策略(Parameter Policy)。路由引數策略通過一個標記介面(不具有任何成員的介面)IParameterPolicy表示路由引數策略,如下所示的RoutePatternParameterPolicyReference是對IParameterPolicy物件的進一步封裝,它定義的Content屬性表示策略的原始(字串)表現形式。應用在路由引數上的策略定義體現在RoutePatternParameterPart的ParameterPolicies屬性上。

public sealed class RoutePatternParameterPolicyReference
{
    public string Content { get; }
    public IParameterPolicy ParameterPolicy { get; }
}

public interface IParameterPolicy
{ }

二、RoutePattern

在瞭解了作為路由模式的基本組成元素RoutePatternPart之後,下面介紹表示路由模式的RoutePattern如何定義。表示路由模式的RoutePattern物件是通過解析路由模板生成的,以字串形式表示的路由模板體現為它的RawText屬性。

public sealed class RoutePattern
{
    public string RawText { get; }
    public IReadOnlyList<RoutePatternPathSegment> PathSegments { get; }
    public IReadOnlyList<RoutePatternParameterPart> Parameters { get; }
    public IReadOnlyDictionary<string, object> Defaults { get; }
    public IReadOnlyDictionary<string, IReadOnlyList<RoutePatternParameterPolicyReference>> ParameterPolicies { get; }

    public decimal InboundPrecedence { get; }
    public decimal OutboundPrecedence { get; }
    public IReadOnlyDictionary<string, object> RequiredValues { get; }

    public RoutePatternParameterPart GetParameter(string name);
}

URL的路徑採用字元“/”作為分隔符,我們將分隔符內的內容稱為段,路由模式下針對路徑段的表示體現在如下所示的RoutePatternPathSegment型別上。RoutePatternPathSegment型別的Parts屬性返回一個RoutePatternPart物件的集合,表示構成該路徑段的基本元素。如果RoutePatternPathSegment的Parts集合只包含一個元素(一般為靜態文字或者路由引數),那麼它被視為一個簡短的路徑段,其IsSimple屬性會返回True。

public sealed class RoutePatternPathSegment
{
    public IReadOnlyList<RoutePatternPart>Parts { get; }
    public bool IsSimple { get; }
}

路由引數是路由模式的一個重要組成部分,RoutePattern的Parameters屬性返回的RoutePatternParameterPart列表是對所有路由引數的描述。路由引數的預設值會存放在Defaults屬性表示的字典中,該字典物件的Key為路由引數的名稱。RoutePattern的ParameterPolicies屬性同樣返回一個字典物件,針對每個路由引數的引數策略被存放到該字典中。藉助RoutePattern型別的GetParameter方法,我們可以通過指定路由引數的名稱得到對應的RoutePatternParameterPart物件。

應用具有一個全域性的路由表,其中包含若干註冊的通過RoutePattern表示的路由模式,無論是入棧方向上針對請求URL的路由解析,還是出棧方向上生成完整的URL,都需要從這個路由表中選擇一個匹配的模式。如果註冊的路由很多,就可能出現多個路由在模式上都與當前上下文匹配的情況,在這種狀況下就需要為註冊的路由模式指定不同的匹配的權重或者優先選擇一個匹配度最高的路由模式,RoutePattern型別的InboundPrecedence屬性和OutboundPrecedence屬性分別代表當前路由模式針對兩個路由方向上的匹配優先順序,數值越大表示匹配度越高。

RoutePattern型別的RequiredValues屬性與出棧URL的生成相關。“weather/{city=010}/{days=4}”是本章開篇例項演示中定義的一個路由模板,如果根據指定的路由引數值(city=010,days=4)生成一個完整的URL,由於提供的路由引數值為預設值,所以生成的如下所示的3個URL路徑都是合法的。具體生成哪一種由RequiredValues屬性來決定,該屬性返回的字典中存放了生成URL時必須指定的路由引數預設值。

  • weather。
  • weather/010。
  • weather/010/4。

三、RoutePatternFactory

靜態型別RoutePatternFactory提供的一系列靜態方法可以幫助我們根據路由模板字串建立表示路由模式的RoutePattern物件。如下所示的3個靜態Parse方法過載幫助我們根據指定的路由模板和其他相關資料,包括路由引數的預設值和引數策略,以及必需的路由引數值(對應RoutePattern的RequiredValues屬性),生成了一個表示路由模式的RoutePattern物件。

public static class RoutePatternFactory
{
    public static RoutePattern Parse(string pattern);
    public static RoutePattern Parse(string pattern, object defaults, object parameterPolicies);
    public static RoutePattern Parse(string pattern, object defaults, object parameterPolicies, object requiredValues);
    ...
}

下面通過一個簡單的例項演示如何利用RoutePatternFactory物件解析指定的路由模板,並生成一個表示路由模式的RoutePattern物件。我們在一個ASP.NET Core應用程式中定義瞭如下所示的Format方法,該方法將指定的RoutePattern物件格式化成一個字串。

public class Program
{
    private static string Format(RoutePattern pattern)
    {           
        var builder = new StringBuilder();
        builder.AppendLine($"RawText:{pattern.RawText}");
        builder.AppendLine($"InboundPrecedence:{pattern.InboundPrecedence}");
        builder.AppendLine($"OutboundPrecedence:{pattern.OutboundPrecedence}");
        var segments = pattern.PathSegments;
        builder.AppendLine("Segments");
        foreach (var segment in segments)
        {
            foreach (var part in segment.Parts)
            {
                builder.AppendLine($"\t{ToString(part)}");
            }
        }
        builder.AppendLine("Defaults");
        foreach (var @default in pattern.Defaults)
        {
            builder.AppendLine($"\t{@default.Key} = {@default.Value}");
        }

        builder.AppendLine("ParameterPolicies ");
        foreach (var policy in pattern.ParameterPolicies)
        {
            builder.AppendLine($"\t{policy.Key} = {string.Join(',', policy.Value.Select(it => it.Content))}");
        }

        builder.AppendLine("RequiredValues");
        foreach (var required in pattern.RequiredValues)
        {
            builder.AppendLine($"\t{required.Key} = {required.Value}");
        }

        return builder.ToString();

        static string ToString(RoutePatternPart part)
        {
            if (part is RoutePatternLiteralPart literal)
            {
                return $"Literal: {literal.Content}";
            }
            if (part is RoutePatternSeparatorPart separator)
            {
                return $"Separator: {separator.Content}";
            }
            else
            {
                var parameter = (RoutePatternParameterPart)part;
                return $"Parameter: Name = {parameter.Name}; Default = {parameter.Default}; 
                    IsOptional = {parameter.IsOptional}; 
                    IsCatchAll = {parameter.IsCatchAll}; 
                    ParameterKind = {parameter.ParameterKind}";
            }
        }
    }
}

在如下所示的應用承載程式中,我們呼叫RoutePatternFactory 型別的靜態方法Parse解析指定的路由模板“weather/{city:regex(^0\d{{2,3}}$)=010}/{days:int:range(1,4)=4}/{detailed?}”,並生成一個RoutePattern物件,該方法呼叫中還指定了requiredValues引數的值。我們呼叫IApplicationBuilder物件的Run方法註冊了唯一的中介軟體,它會呼叫上面定義的Format方法將生成的RoutePattern物件格式化成字串,並作為最終的響應內容。

public class Program
{
    public static void Main()
    {
        var template = @"weather/{city:regex(^0\d{{2,3}}$)=010}/{days:int:range(1,4)=4}/{detailed?}";
        var pattern = RoutePatternFactory.Parse(
            pattern: template,
            defaults: null,
            parameterPolicies: null,
            requiredValues: new { city = "010", days = 4 });
           
        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(builder => builder.Configure(app => app.Run(context => context.Response.WriteAsync(Format(pattern)))))
            .Build()
            .Run();            
    }
}

如果利用瀏覽器訪問啟動後的應用程式,得到的輸出結果如下圖所示,該結果結構化地展示了路由模式的原始文字、出入棧路由匹配權重、每個段的組成、路由引數的預設值和引數策略,以及生成URL必須提供的預設引數值。

除了提供Parse方法解析指定的路由模板並生成表示路由模式的RoutePattern物件,RoutePatternFactory還提供了用於解析其他與路由模式相關物件的靜態方法,這些物件包括表示路徑段的RoutePatternPathSegment物件、針對路由引數的RoutePatternParameterPart物件、針對引數策略的RoutePatternParameterPolicyReference物件等。由於篇幅有限,此處不再一一列舉。

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