ASP.NET Core的路由[5]:内联路由约束的检验

当某个请求能够被成功路由的前提是它满足某个Route对象设置的路由规则,具体来说,当前请求的URL不仅需要满足路由模板体现的路径模式,请求还需要满足Route对象的所有约束。路由系统采用IRouteConstraint接口来表示路由约束,所以我们在接下来的内容中将路由约束统称为RouteConstraint。 在大部分情况下,约束都是针对路由模板中定义的某个路由参数,其目的在于验证URL携带的某部分的内容是否有效。不过也有一些约束与路由参数无关,这些约束规范往往是除URL之前的其他请求元素,比如前面提到的HttpMethodRouteConstraint检验的就是请求采用的方法。 [本文已经同步到《ASP.NET Core框架揭秘》之中]

   1: public interface IRouteConstraint
   2: {
   3:     bool Match(HttpContext httpContext,IRouter route,string routeKey,
   4:     RouteValueDictionary values,RouteDirection routeDirection);
   5: }

如上面的代码片段所示,IRouteConstraint接口仅仅定义了如下一个唯一的Match方法来定义约束规范。方法的参数分别是代表当前请求上下文的HttpContext、当前Router对象、约束在约束字典中的Key(对于针对路由参数的约束,这个Key就是路由参数的名称)、从请求URL解析出来的所有路由参数和路由方向(针对入栈请求进行的路由解析还是为了生成URL而进行的路由解析)。

一、预定义RouteConstraint

路由系统定义了一系列原生的RouteConstraint类型,我们可以使用它们解决很多常见的约束问题,即使现有的RouteConstraint类型无法满足某些特殊的约束需求,我们还可以自定义对应的RouteConstraint类型。对于路由约束的应用,除了直接创建对应的RouteConstraint对象之外,我们知道还可以采用内联的方式直接在路由模板中定义为某个路由参数定义相应的约束表达式。这些以表达式定义的约束类型其实对应着一种具体的RouteConstraint类型。下表列出了两者之间的匹配关系。

内联约束类型

RouteConstraint类型

说明

int

IntRouteConstraint

要求路由参数值可能解析为一个int整数,比如{variable:int}

bool

BoolRouteConstraint

要求参数值可以解析为一个bool值,比如{ variable:bool}

datetime

DateTimeRouteConstraint

要求参数值可以解析为一个DateTime对象(采用CultureInfo. InvariantCulture进行解析),比如{ variable:datetime}

decimal

DecimalRouteConstraint

要求参数值可以解析为一个decimal数字,比如{ variable:decimal}

double

DoubleRouteConstraint

要求参数值可以解析为一个double数字,比如{ variable:double}

float

FloatRouteConstraint

要求参数值可以解析为一个float数字,比如{ variable:float}

guid

GuidRouteConstraint

要求参数值可以解析为一个Guid,比如{ variable:guid}

long

LongRouteConstraint

要求参数值可以解析为一个long整数,比如{ variable:long}

minlength

MinLengthRouteConstraint

要求参数值表示的字符串不于指定的长度{ variable:minlength(5)}

maxlength

MaxLengthRouteConstraint

要求参数值表示的字符串不大于指定的长度,比如{ variable:maxlength(10)}

length

LengthRouteConstraint

要求参数值表示的字符串长度限于指定的区间范围,比如{ variable:length(5,10)}

min

MinRouteConstraint

要求参数值不于指定的值,比如{ variable:min(5)}

max

MaxRouteConstraint

要求参数值大于指定的值,比如{ variable:max(10)}

range

RangeouteConstraint

要求参数值介于指定的区间范围,比如{variable:range(5,10)}

alpha

AlphaRouteContraint

要求参数值得所有字符都是字母,比如{variable:alpha}

regex

RegexInlineRouteConstraint

要求参数值表示字符串与指定的正则表达式相匹配,比如{variable:regex(^d{0[0-9]{{2,3}-d{2}-d{4}$)}}}$)}

required

RequiredRouteConstraint

要求参数值不应该是一个空字符串,比如{variable:required}

RangeRouteConstraint

为了让读者朋友们对这些RouteConstraint具有更加深刻的理解,我们选择一个用于限制变量值范围的RangeRouteConstraint类进行单独介绍。如下面的代码片断所示,RangeRouteConstraint类型具有两个长整型的只读属性Max和Min,它们分别表示约束范围的上下限。

long Max { get;  }
   5:     public RangeRouteConstraint(long min,1)">long max)
   6:     {
   7:         this.Min = min;
   8:         this.Max = max;
   9:     }
  10:  
  11:       12:     {
  13:         object value;
  14:         if (values.TryGetValue(routeKey,1)">out value) && value != null)
  15:         {
  16:             long longValue;
  17:             var valueString = Convert.ToString(value,CultureInfo.InvariantCulture);
  18:             if (long.TryParse(valueString,NumberStyles.Integer,CultureInfo.InvariantCulture,1)">out longValue))
  19:             {
  20:                 return longValue >= Min && longValue <= Max;
  21:             }
  22:         }
  23:         return false;
  24:     }
  25: }

具体的约束检验实现在Match方法中。具体来说,RangeRouteConstraint根据被检验变量的名称(对应于routeKey参数)从参数values(表示路由检验生成的所有路由变量)中提取被验证的参数值,然后判断它是否在通过属性Max和Min表示的数值范围内。

HttpMethodRouteConstraint

上面介绍的这些预定义的RouteConstraint类型都是对某个路由参数的值加以约束,除此之外还具有一个特殊的名为HttpMethodRouteConstraint的约束。我们在上面已经提到过,这个约束并不是应用在具有某个路由参数上,而是应用到整个请求上,它要求匹配的请求必须具有指定的方法。当我们在使用这种约束的时候,一般将对应的Key设置为“httpMethod”。

public IList<string> AllowedMethods { get; }
public HttpMethodRouteConstraint(params string[] allowedMethods)
   8:     }
  10:     virtual   11:     {        
  13:         {
  15:  
  17:                 object obj;
  19:                 {
  21:                 }
  24:             default:throw new ArgumentOutOfRangeException(nameof(routeDirection));
  26:     }
  27: }

当我们在创建一个 HttpMethodRouteConstraint对象的时候,需要指定一个允许的HTTP方法列表。对于针对入栈请求的路由解析来说,HttpMethodRouteConstraint会检验当前请求采用的方法是否在这个列表之内。如果路由解析是为了生成URL,HttpMethodRouteConstraint会从指定的参数列表中提取指定的HTTP方法,如果这样的参数存在,则会检验这个HTTP方法是否在允许的列表之内,否则意味着不需要针对HTTP方法进行验证。

二、InlineConstraintResolver

如果在进行路由注册的时候针对路由变量的约束是直接以内联表达式的形式定义在路由模板中,所以路由系统需要解析约束表达式来创建对应类型的RouteConstraint对象,这项任务由一个叫做InlineConstraintResolver的对象来完成。所有的InlineConstraintResolver类型实现了具有如下定义的IInlineConstraintResolver接口,定义其中的唯一方法ResolveConstraint实现了约束从字符串表达式到RouteConstraint对象之间的转换。

2: {  
   4: }

路由系统只定义了一个唯一的InlineConstraintResolver类型实现了这个接口,它就是DefaultInlineConstraintResolver类型。如下面的代码片断所示,它具有一个字典类型的字段_inlineConstraintMap,如表1所示的内联约束类型与对应RouteConstraint类型之间的映射关系就保存在这个字典中。

private readonly IDictionary<string,Type> _inlineConstraintMap;
   5:     {
   7:     }
  11: class RouteOptions
  13:     public IDictionary<  14:     bool                      LowercaseUrls { get; set; }
  16: }

DefaultInlineConstraintResolver首先根据指定的约束表达式获得以字符串表示的约束类型和参数列表。通过约束类型,它可以从ConstraintMap属性表示的映射关系中得到对应的HttpRouteConstraint类型。接下来它根据参数个数得到匹配的构造函数,然后将字符串表示的参数转换成对应的参数类型并以反射的形式将它们传入构造函数创建相应的HttpRouteConstraint对象。

对于一个通过指定的路由模板创建的Route对象来说,当它在初始化的时候会利用ServiceProvider采用依赖注入的形式获取这个InlineConstraintResolver对象来解析定义在路由模板中的内联约束表达式,并将它们全部转换成具体的RouteConstraint对象。这意味着在这之前,针对InlineConstraintResolver的服务注册就以及存在,那么这个服务是在什么时候注册的呢?

当我们在一个ASP.NET Core应用中使用路由功能的时候,除了需要注册这个RouterMiddleware中间件之外,一般还需要调用ServiceCollection的扩展方法AddRouting注册一些与路由相关的服务,针对InlineConstraintResolver的服务注册就实现在这个方法之中。

三、自定义约束

我们可以使用上述这些预定义的RouteConstraint类们完成一些常用的约束检验,但是在一些对路由变量具有特殊的约束的应用场景中,我们不得不创建自定义的约束。举个简单的例子,如果我们需要对资源提供针对多语言的支持,最好的方式是在请求的URL中提供目标资源所针对的Culture。为了确保包含在URL中的是一个合法有效的Culture,我们最好为此定义相应的约束。

接下来,我们将通过一个简单的实例来演示如何创建这么一个用于验证Culture的自定义约束。不过在这之前我们不妨先来看看使用这个约束最终实现的效果。在本例中我们创建了一个提供基于不同语言资源的Web API,简单起见,我们仅仅提供针对相应Culture的文本数据。我们利用资源文件来作为文本资源的存储,如下图所示,我们在一个ASP.NET Core应用中创建了两个资源文件Resources.resx(语言文化中性)和Resources.zh.resx(中文),并定义了一个名为“hello”的文本资源条目。

10

如下所示的是整个应用程序的定义。这段程序非常简单,我们注册了一个模板为“resources/{lang:culture}/{resourcename:required}”的路由。路由参数{ resourcename }表示获取的资源条目的名称(比如“hello”),这是一个必需的路由参数(应用了RequiredRouteConstraint约束)。另一个路由参数{lang}表示指定的语言,约束表达式名称“culture”对应的就是我们自定义的针对语言文件的约束类型CultureConstraint。也正是因为是一个自定义的路由约束,我们必须将内联约束表达式名称和CultureConstraint类型之间的应用,我们在调用ConfigureServices方法中将这样的映射添加到注册的RouteOptions之中。

static void Main()
   5:         string template = "resources/{lang:culture}/{resourceName:required}";
   7:         Action<IApplicationBuilder> action = app => app
   9:             .Run(async context =>
  11:                 var values = context.GetRouteData().Values;
  13:                 await context.Response.WriteAsync(Resources.ResourceManager.GetString(resourceName));
  16:         new WebHostBuilder()
  18:             .ConfigureServices(svcs => svcs
  20:                 .Configure<RouteOptions>(options=>options.ConstraintMap.Add("culture",1)">typeof(CultureConstraint))))
  22:             .Build()
  25: }

我们通过调用扩展方法MapRoute注册了这个路由。利用作为参数的Action<IApplicationBuilder>对象,我们注册了一个自定义的LocalizationMiddleware中间件,这个中间件实现针对多语言的本地化。至于资源内容的响应,我们将它实现在通过调用ApplicationBuilder的Run方法注册的中间件上。我们从解析出来的路由参数中获取目标资源条目的名称,然后利用资源文件自动生成的Resoruces类型获取对应的资源内容并响应给客户端。

在揭秘CultureConstraint这个自定义路由约束以及LocalizationMiddleware中间件的实现原理之前,我们先来看看客户端采用是采用怎样的形式获取某个资源条目针对某种语言的内容。如下图所示,我们直接利用浏览器采用与注册路由相匹配的URL(“/resources/en/hello”或者“/resources/zh/hello”)不仅可以获取目标资源的内容,显示的语言也与我们指定的语言文化一致。如果指定一个不合法的语言(比如“xx”),将会违反我们自定义的约束,此时就会得到一个状态码为“404 Not Found”的响应。

11

 

接下来我们来看看这个针对语言文化的路由约束CultureConstraint就是做了些什么。如下面的代码片段所示,我们在Match方法中会试图获取作为语言文化内容的路由参数值,如果这样的路由参数存在,我们会利用它创建一个CultureInfo对象。如果这个CultureInfo的EnglishName属性名不以“Unknown Language”字符作为前缀,我们就认为指定的是合法的语言文件。

try
   7:                8:             value))
  10:                 return !new CultureInfo(value.ToString()).EnglishName.StartsWith("Unknown Language");
  12:               13:         }
  17:         }
  19: }

我们.NET应用在运行的时候具有根据当前线程的语言文化选择资源文件的能力。就我们这实例提供的两个资源文件(Resources.resx和Resources.zh.resx)来说,如果当前线程的UICulture属性代表的是一个针对“zh”的语言文化,资源文件Resources.zh.resx会被选择。对于其他语言文件,则被选择的就是这个Resources.resx文件。换句话说,如果我们要让运行时选择某个我们希望的资源文件,我们可以为当前线程设置相应的语言文化,实际上LocalizationMiddleware这个中间件就是这么做的。

private RequestDelegate     _next;
   5:  
   7:     {
   9:         _routeKey = routeKey;
  11:  
  13:     {
  15:         CultureInfo currentCulture = CultureInfo.CurrentCulture;
  17:           18:         {
  20:             {
  22:             }
  24:         }
  26:         {
  28:             CultureInfo.CurrentUICulture = currentUICulture;
  29:         }
  30:     }
  31: }

如上面的代码片段所示,LocalizationMiddleware的Invoke方法被执行的时候,它会试图从路由参数中得到目标语言,代表路由参数名称的字段_routeKey是在构造函数中初始化的。如果这样的路由参数存在,它会据此创建一个CultureInfo对象并将其作为当前线程的Culture和CultureInfo属性。值得一提的是,在完成后续请求处理流程之后,我们需要将当前线程的语言文化恢复到之前的状态。

 

ASP.NET Core的路由[1]:注册URL模式与HttpHandler的映射关系
ASP.NET Core的路由[2]:路由系统的核心对象——Router
ASP.NET Core的路由[3]:Router的创建者——RouteBuilder
ASP.NET Core的路由[4]:来认识一下实现路由的RouterMiddleware中间件
ASP.NET Core的路由[5]:内联路由约束的检验

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


在上文中,我介绍了事件驱动型架构的一种简单的实现,并演示了一个完整的事件派发、订阅和处理的流程。这种实现太简单了,百十行代码就展示了一个基本工作原理。然而,要将这样的解决方案运用到实际生产环境,还有很长的路要走。今天,我们就研究一下在事件处理器中,对象生命周期的管理问题。事实上,不仅仅是在事件处理器
上文已经介绍了Identity Service的实现过程。今天我们继续,实现一个简单的Weather API和一个基于Ocelot的API网关。 回顾 《Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(一)》 Weather API Weather
最近我为我自己的应用开发框架Apworks设计了一套案例应用程序,并以Apache 2.0开源,开源地址是:https://github.com/daxnet/apworks-examples,目的是为了让大家更为方便地学习和使用.NET Core、最新的前端开发框架Angular,以及Apwork
HAL(Hypertext Application Language,超文本应用语言)是一种RESTful API的数据格式风格,为RESTful API的设计提供了接口规范,同时也降低了客户端与服务端接口的耦合度。很多当今流行的RESTful API开发框架,包括Spring REST,也都默认支
在前面两篇文章中,我详细介绍了基本事件系统的实现,包括事件派发和订阅、通过事件处理器执行上下文来解决对象生命周期问题,以及一个基于RabbitMQ的事件总线的实现。接下来对于事件驱动型架构的讨论,就需要结合一个实际的架构案例来进行分析。在领域驱动设计的讨论范畴,CQRS架构本身就是事件驱动的,因此,
HAL,全称为Hypertext Application Language,它是一种简单的数据格式,它能以一种简单、统一的形式,在API中引入超链接特性,使得API的可发现性(discoverable)更强,并具有自描述的特点。使用了HAL的API会更容易地被第三方开源库所调用,并且使用起来也很方便
何时使用领域驱动设计?其实当你的应用程序架构设计是面向业务的时候,你已经开始使用领域驱动设计了。领域驱动设计既不是架构风格(Architecture Style),也不是架构模式(Architecture Pattern),它也不是一种软件开发方法论,所以,是否应该使用领域驱动设计,以及什么时候使用
《在ASP.NET Core中使用Apworks快速开发数据服务》一文中,我介绍了如何使用Apworks框架的数据服务来快速构建用于查询和管理数据模型的RESTful API,通过该文的介绍,你会看到,使用Apworks框架开发数据服务是何等简单快捷,提供的功能也非常多,比如对Hypermedia的
在上一讲中,我们已经完成了一个完整的案例,在这个案例中,我们可以通过Angular单页面应用(SPA)进行登录,然后通过后端的Ocelot API网关整合IdentityServer4完成身份认证。在本讲中,我们会讨论在当前这种架构的应用程序中,如何完成用户授权。 回顾 《Angular SPA基于
Keycloak是一个功能强大的开源身份和访问管理系统,提供了一整套解决方案,包括用户认证、单点登录(SSO)、身份联合、用户注册、用户管理、角色映射、多因素认证和访问控制等。它广泛应用于企业和云服务,可以简化和统一不同应用程序和服务的安全管理,支持自托管或云部署,适用于需要安全、灵活且易于扩展的用
3月7日,微软发布了Visual Studio 2017 RTM,与之一起发布的还有.NET Core Runtime 1.1.0以及.NET Core SDK 1.0.0,尽管这些并不是最新版,但也已经从preview版本升级到了正式版。所以,在安装Visual Studio 2017时如果启用了
在上文中,我介绍了如何在Ocelot中使用自定义的中间件来修改下游服务的response body。今天,我们再扩展一下设计,让我们自己设计的中间件变得更为通用,使其能够应用在不同的Route上。比如,我们可以设计一个通用的替换response body的中间件,然后将其应用在多个Route上。 O
不少关注我博客的朋友都知道我在2009年左右开发过一个名为Apworks的企业级应用程序开发框架,旨在为分布式企业系统软件开发提供面向领域驱动(DDD)的框架级别的解决方案,并对多种系统架构风格提供支持。这个框架的开发和维护我坚持了很久,一直到2015年,我都一直在不停地重构这个项目。目前这个项目在
好吧,这个题目我也想了很久,不知道如何用最简单的几个字来概括这篇文章,原本打算取名《Angular单页面应用基于Ocelot API网关与IdentityServer4ʺSP.NET Identity实现身份认证与授权》,然而如你所见,这样的名字实在是太长了。所以,我不得不缩写“单页面应用”几个字
在前面两篇文章中,我介绍了基于IdentityServer4的一个Identity Service的实现,并且实现了一个Weather API和基于Ocelot的API网关,然后实现了通过Ocelot API网关整合Identity Service做身份认证的API请求。今天,我们进入前端开发,设计
Ocelot是ASP.NET Core下的API网关的一种实现,在微服务架构领域发挥了非常重要的作用。本文不会从整个微服务架构的角度来介绍Ocelot,而是介绍一下最近在学习过程中遇到的一个问题,以及如何使用中间件(Middleware)来解决这样的问题。 问题描述 在上文中,我介绍了一种在Angu
在大数据处理和人工智能时代,数据工厂(Data Factory)无疑是一个非常重要的大数据处理平台。市面上也有成熟的相关产品,比如Azure Data Factory,不仅功能强大,而且依托微软的云计算平台Azure,为大数据处理提供了强大的计算能力,让大数据处理变得更为稳定高效。由于工作中我的项目
在上文中,我们讨论了事件处理器中对象生命周期的问题,在进入新的讨论之前,首先让我们总结一下,我们已经实现了哪些内容。下面的类图描述了我们已经实现的组件及其之间的关系,貌似系统已经变得越来越复杂了。其中绿色的部分就是上文中新实现的部分,包括一个简单的Event Store,一个事件处理器执行上下文的接
在之前《在ASP.NET Core中使用Apworks快速开发数据服务》一文的评论部分,.NET大神张善友为我提了个建议,可以使用Compile As a Service的Roslyn为语法解析提供支持。在此非常感激友哥给我的建议,也让我了解了一些Roslyn的知识。使用Roslyn的一个很大的好处
很长一段时间以来,我都在思考如何在ASP.NET Core的框架下,实现一套完整的事件驱动型架构。这个问题看上去有点大,其实主要目标是为了实现一个基于ASP.NET Core的微服务,它能够非常简单地订阅来自于某个渠道的事件消息,并对接收到的消息进行处理,于此同时,它还能够向该渠道发送事件消息,以便