ASP.NET Core路由中间件[2]: 路由模式

一个Web应用本质上体现为一组终结点的集合。终结点则体现为一个暴露在网络中可供外界采用HTTP协议调用的服务,路由的作用就是建立一个请求URL模式与对应终结点之间的映射关系。借助这个映射关系,客户端可以采用模式匹配的URL来调用对应的终结点。除了利用下图所示的映射关系对请求进行路由解析,然后选择并执行与之匹配的终结点,路由系统还可以注册路由的URL模式和指定的路由参数值生成一个完整的URL。我们将这两方面的工作称为两个路由方向(Routing Direction),前者为入栈路由(Inbound Routing),后者为出栈路由(Outbound Routing)。[更多关于ASP.NET Core的文章请点这里]

ASP.NET Core路由中间件[2]: 路由模式

对于路由系统来说,作为路由目标的终结点总是关联一个具体的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必须提供的默认参数值。

ASP.NET Core路由中间件[2]: 路由模式

除了提供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]: 路由约束

发表评论

评论已关闭。

相关文章