一个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]: 路由约束

ASP.NET Core路由中间件[2]: 路由模式的更多相关文章

  1. asp.net core mvc 中间件之路由

    asp.net core mvc 中间件之路由 路由中间件 首先看路由中间件的源码 先用httpContext实例化一个路由上下文,然后把中间件接收到的路由添加到路由上下文的路由集合 然后把路由上下文 ...

  2. ASP.NET Core:中间件

    一.什么是中间件 我们都知道,任何的一个web框架都是把http请求封装成一个管道,每一次的请求都是经过管道的一系列操作,最终才会到达我们写的代码中.而中间件就是用于组成应用程序管道来处理请求和响应的 ...

  3. 如何传递参数给ASP.NET Core的中间件(Middleware)

    问题描述 当我们在ASP.NET Core中定义和使用中间件(Middleware)的时候,有什么好的办法可以给中间件传参数吗? 解决方案 在ASP.NET Core项目中添加一个POCO类来传递参数 ...

  4. asp.net core mvc 中间件之WebpackDevMiddleware

    asp.net core mvc 中间件之WebpackDevMiddleware WebpackDevMiddleware中间件主要用于开发SPA应用,启用Webpack,增强网页开发体验.好吧,你 ...

  5. 如何在ASP.NET Core自定义中间件中读取Request.Body和Response.Body的内容?

    原文:如何在ASP.NET Core自定义中间件中读取Request.Body和Response.Body的内容? 文章名称: 如何在ASP.NET Core自定义中间件读取Request.Body和 ...

  6. asp.net core 使用中间件拦截请求和返回数据,并对数据进行加密解密。

    原文:asp.net core 使用中间件拦截请求和返回数据,并对数据进行加密解密. GitHub demo https://github.com/zhanglilong23/Asp.NetCore. ...

  7. ASP.NET Core中使用自定义路由

    上一篇文章<ASP.NET Core中使用默认MVC路由>提到了如何使用默认的MVC路由配置,通过这个配置,我们就可以把请求路由到Controller和Action,通常情况下我们使用默认 ...

  8. (8)ASP.NET Core 中的MVC路由一

    1.前言 ASP.NET Core MVC使用路由中间件来匹配传入请求的URL并将它们映射到操作(Action方法).路由在启动代码(Startup.Configure方法)或属性(Controlle ...

  9. ASP.NET Core MVC 配置全局路由前缀

    前言 大家好,今天给大家介绍一个 ASP.NET Core MVC 的一个新特性,给全局路由添加统一前缀.严格说其实不算是新特性,不过是Core MVC特有的. 应用背景 不知道大家在做 Web Ap ...

随机推荐

  1. 第3.3节 强大的Python列表

    一. 列表切片操作补充 列表切片支持所有序列切片的方法,以倒序切片和步长大于1的情况再举例验证一下: l=[1,2,3,4,5] l[::2] #结果[1, 3, 5] l[-1::2] #结果[5] ...

  2. Python中判断一个中文是否中文数字的方法

    Python内置功能非常强大,在字符串内置函数中提供了一个判断字符串是否全数字的方法,而且这个方法不只是简单判断阿拉伯数字,包括中文数字和全角的阿拉伯数字都认识,这个函数就是字符串的isnumeric ...

  3. PLSQL Developer 工具应用

    用户scott使用: 解锁scott: 第一步:登陆管理员 SQL语句:Sqlplus sys/tiger as sysdba 第二步:解锁scott SQL语句:Alter user scott a ...

  4. Java 8 中的方法引用,轻松减少代码量,提升可读性!

    1. 引言 Java8中最受广大开发中喜欢的变化之一是因为引入了 lambda 表达式,因为这些表达式允许我们放弃匿名类,从而大大减少了样板代码,并提高了可读性. 方法引用是lambda表达式的一种特 ...

  5. pytorch知识(torch.sum,以及维度问题)

    参考(推荐): https://mathpretty.com/12065.html

  6. 学习JUC源码(1)——AQS同步队列(源码分析结合图文理解)

    前言 最近结合书籍<Java并发编程艺术>一直在看AQS的源码,发现AQS核心就是:利用内置的FIFO双向队列结构来实现线程排队获取int变量的同步状态,以此奠定了很多并发包中大部分实现基 ...

  7. I/O-基本概念

    目录 演变过程 I/O系统基本组成 I/O接口 I/O方式简介 小结 演变过程 I/O系统基本组成 分成软件和硬件 I/O接口 接口可以看作是两个部件之间的交接部分 I/O方式简介 小结

  8. 三、LoadRunner卸载

    由于安装的时候没看提前了解清楚,LoadRunner社区版只能模拟50个用户,满足不了工作需求,删了重装吧. 1-打开"控制面板"-点击"卸载程序" 2-找到H ...

  9. vue 属性绑定 v-bind

    属性绑定 v-bind 可以通过v-bind将属性值与数据绑定,这样就可以统一化管理 通过这样我们就可以直接访问跳转到百度页面 同样的这个值我们也可以通过事件进行改变 这样就可以方便我们做一些其它的操 ...

  10. js上 七、表达式

    (1).什么是表达式 任何有值的内容都是表达式 一个表达式会产生一个值,它可以放在任何需要一个值的地方,比如a=3中的3就是一个表达式,a=3整体也可以作为一个表达式. 常见表达式有如下几种: ü 原 ...