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,它们分别表示约束范围的上下限。

   1: public class RangeRouteConstraint : IRouteConstraint
   2: {
   3:     public long Max { get;  }
   4:     public long Min { get; }
   5:     public RangeRouteConstraint(long min, long max)
   6:     {
   7:         this.Min = min;
   8:         this.Max = max;
   9:     }
  10:  
  11:     public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
  12:     {
  13:         object value;
  14:         if (values.TryGetValue(routeKey, 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, 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”。

   1: public class HttpMethodRouteConstraint : IRouteConstraint
   2: {
   3:     public IList<string> AllowedMethods { get; }
   4:  
   5:     public HttpMethodRouteConstraint(params string[] allowedMethods)
   6:     {
   7:         this.AllowedMethods = new List<string>(allowedMethods);
   8:     }
   9:     
  10:     public virtual bool Match(HttpContext httpContext, IRouter route, string routeKey,RouteValueDictionary values, RouteDirection routeDirection)
  11:     {        
  12:         switch (routeDirection)
  13:         {
  14:             case RouteDirection.IncomingRequest:return AllowedMethods.Contains(httpContext.Request.Method, StringComparer.OrdinalIgnoreCase);
  15:  
  16:             case RouteDirection.UrlGeneration:
  17:                 object obj;
  18:                 if (!values.TryGetValue(routeKey, out obj))
  19:                 {
  20:                     return true;
  21:                 }
  22:                 return AllowedMethods.Contains(Convert.ToString(obj), StringComparer.OrdinalIgnoreCase);
  23:  
  24:             default:throw new ArgumentOutOfRangeException(nameof(routeDirection));
  25:         }
  26:     }
  27: }

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

二、InlineConstraintResolver

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

   1: public interface IInlineConstraintResolver
   2: {   
   3:     IRouteConstraint ResolveConstraint(string inlineConstraint);
   4: }

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

   1: public class DefaultInlineConstraintResolver : IInlineConstraintResolver
   2: {
   3:     private readonly IDictionary<string, Type> _inlineConstraintMap;
   4:     public DefaultInlineConstraintResolver(IOptions<RouteOptions> routeOptions)
   5:     {
   6:         _inlineConstraintMap = routeOptions.Value.ConstraintMap;
   7:     }
   8:     public virtual IRouteConstraint ResolveConstraint(string inlineConstraint);
   9: }
  10:  
  11: public class RouteOptions
  12: {
  13:     public IDictionary<string, Type> ConstraintMap { get; set; }
  14:     public bool                      LowercaseUrls { get; set; }
  15:     public bool                      AppendTrailingSlash { 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”的文本资源条目。

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

   1: public class Program
   2: {
   3:     public static void Main()
   4:     {
   5:         string template = "resources/{lang:culture}/{resourceName:required}";
   6:  
   7:         Action<IApplicationBuilder> action = app => app
   8:             .UseMiddleware<LocalizationMiddleware>("lang")
   9:             .Run(async context =>
  10:             {
  11:                 var values = context.GetRouteData().Values;
  12:                 string resourceName = values["resourceName"].ToString().ToLower();
  13:                 await context.Response.WriteAsync(Resources.ResourceManager.GetString(resourceName));
  14:             });
  15:  
  16:         new WebHostBuilder()
  17:             .UseKestrel()
  18:             .ConfigureServices(svcs => svcs
  19:                 .AddRouting()
  20:                 .Configure<RouteOptions>(options=>options.ConstraintMap.Add("culture", typeof(CultureConstraint))))
  21:             .Configure(app =>app.UseRouter(builder=> builder.MapRoute(template, action)))
  22:             .Build()
  23:             .Run();
  24:     }
  25: }

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

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

 

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

   1: public class CultureConstraint : IRouteConstraint
   2: {
   3:     public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
   4:     {
   5:         try
   6:         {
   7:             object value;
   8:             if (values.TryGetValue(routeKey, out value))
   9:             {
  10:                 return !new CultureInfo(value.ToString()).EnglishName.StartsWith("Unknown Language");
  11:             }
  12:             return false;
  13:         }
  14:         catch
  15:         {
  16:             return false;
  17:         }
  18:     }
  19: }

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

   1: public class LocalizationMiddleware
   2: {
   3:     private RequestDelegate     _next;
   4:     private string              _routeKey;
   5:  
   6:     public LocalizationMiddleware(RequestDelegate next, string routeKey)
   7:     {
   8:         _next = next;
   9:         _routeKey = routeKey;
  10:     }
  11:  
  12:     public async Task Invoke(HttpContext context)
  13:     {
  14:         object culture;
  15:         CultureInfo currentCulture = CultureInfo.CurrentCulture;
  16:         CultureInfo currentUICulture = CultureInfo.CurrentUICulture;
  17:         try
  18:         {
  19:             if (context.GetRouteData().Values.TryGetValue(_routeKey, out culture))
  20:             {
  21:                 CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = new CultureInfo(culture.ToString());
  22:             }
  23:             await _next(context);
  24:         }
  25:         finally
  26:         {
  27:             CultureInfo.CurrentCulture = currentCulture;
  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]:内联路由约束的检验

作者:蒋金楠
微信公众账号:大内老A
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文链接

时间: 2024-09-14 11:48:41

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

ASP.NET Core的路由[1]:注册URL模式与HttpHandler的映射关系

ASP.NET Core的路由是通过一个类型为RouterMiddleware的中间件来实现的.如果我们将最终处理HTTP请求的组件称为HttpHandler,那么RouterMiddleware中间件的意义在于实现请求路径与对应HttpHandler之间的映射关系.对于传递给RouterMiddleware中间件的每一个请求,它会通过分析请求URL的模式并选择并提取对应的HttpHandler来处理该请求.除此之外,请求的URL还会携带相应参数,该中间件在进行路由解析过程中还会根据生成相应的路

ASP.NET Core的路由[3]:Router的创建者——RouteBuilder

在<注册URL模式与HttpHandler的映射关系>演示的实例中,我们总是利用一个RouteBuilder对象来为RouterMiddleware中间件创建所需的Router对象,接下来我们就着重来介绍这个对象.RouteBuilder是我们对所有实现了IRouteBuilder接口的所有类型以及对应对象的统称.[本文已经同步到<ASP.NET Core框架揭秘>之中] 目录 一.RouteBuilder 二.RouteCollection 三.多个Route共享同一个Handl

ASP.NET Core的路由[4]:来认识一下实现路由的RouterMiddleware中间件

虽然ASP.NET Core应用的路由是通过RouterMiddleware这个中间件来完成的,但是具体的路由解析功能都落在指定的Router对象上,不过我们依然有必要以代码实现的角度来介绍一下这个中间件.在这之前,我们先来认识一个特殊的特性.[本文已经同步到<ASP.NET Core框架揭秘>之中] 让RouterMiddleware中间件委托Router完整整个路由工作之后,解析出来的路由参数会以一个RouteData对象的形式存储在RouteContext上下文中.但是RouteCont

ASP.NET Core的路由[2]:路由系统的核心对象——Router

ASP.NET Core应用中的路由机制实现在RouterMiddleware中间件中,它的目的在于通过路由解析为请求找到一个匹配的处理器,同时将请求携带的数据以路由参数的形式解析出来供后续请求处理流程使用.但是具体的路由解析功能其实并没有直接实现在RouterMiddleware中间件中,而是由一个Router对象来完成的.[本文已经同步到<ASP.NET Core框架揭秘>之中] 目录 一.IRouter接口 二.RouteContext 三.RouteData 四.Route 五.Rou

ASP.NET Core框架揭秘(持续更新中…)

之前写了一系列关于.NET Core/ASP.NET Core的文章,但是大都是针对RC版本.到了正式的RTM,很多地方都发生了改变,所以我会将之前发布的文章针对正式版本的.NET Core 1.0进行改写.除此之外,我还会撰写一系列与此相关的文章,这些文章以ASP.NET Core为核心,我个人将它们分成三个主要的部分,即编程基础.支撑框架和管道详解.其中编程基础主要涉及与ASP.NET Core独特的编程模型和相关编程技巧.支撑框架则介绍支撑ASP.NET Core的多个独立的框架,比如依赖

ASP.NET 2.0后台代码与内联代码的对比

asp.net|后台 内联(Inline)代码的分离 下面的例子演示了一个简单的带有三个服务器控件(分别是文本框.按钮和标签)的ASP.NET页面.最初这些控件呈现的内容与HTML形式是相同的.但是,当你在客户端的文本框中输入值并点击按钮的时候,该页面会发回服务器并且在该页面的代码中处理这个点击事件,动态地更新标签控件的Text属性.接下来这个页面会重新呈现以反映更新过的文本.这个简单的例子演示了服务器控件模型背后的基本原理,它使ASP.NET成为最容易学习和掌握的Web编程模型之一. <%@

ASP.NET Core MVC 配置全局路由前缀_实用技巧

ASP.NET Core MVC 配置全局路由前缀 前言 大家好,今天给大家介绍一个 ASP.NET Core MVC 的一个新特性,给全局路由添加统一前缀.严格说其实不算是新特性,不过是Core MVC特有的. 应用背景 不知道大家在做 Web Api 应用程序的时候,有没有遇到过这种场景,就是所有的接口都是以 /api 开头的,也就是我们的api 接口请求地址是像这样的: http://www.example.com/api/order/333 或者是这样的需求 http://www.exa

ASP.NET Core MVC/WebAPi如何构建路由?

前言 本节我们来讲讲ASP.NET Core中的路由,在讲路由之前我们首先回顾下之前所讲在ASP.NET Core中的模型绑定这其中有一个问题是我在项目当中遇见的,我们下面首先来看看这个问题. 回顾ASP.NET Core模型绑定 我们有这样一个场景:修改个人资料中的各个属性,此时每个属性的值的类型肯定是不一样的,所以我们将值定义为object,如下model. public class BlogViewModel { public string prop { get; set; } publi

详解ASP.NET Core 之 Identity 入门(二)_实用技巧

前言 在 上篇文章 中讲了关于 Identity 需要了解的单词以及相对应的几个知识点,并且知道了Identity处在整个登入流程中的位置,本篇主要是在 .NET 整个认证系统中比较重要的一个环节,就是 认证(Authentication),因为想要把 Identity 讲清楚,是绕不过 Authentication 的. 其实 Identity 也是认证系统的一个具体使用,大家一定要把 Authentication 和 Identity 当作是两个东西,一旦混淆,你就容易陷入进去. 下面就来说