注册URL模式与HttpHandler的映射关系

ASP.NET Core的路由是通过一个类型为RouterMiddleware的中间件来实现的。如果我们将最终处理HTTP请求的组件称为HttpHandler,那么RouterMiddleware中间件的意义在于实现请求路径与对应HttpHandler之间的映射关系。对于传递给RouterMiddleware中间件的每一个请求,它会通过分析请求URL的模式并选择并提取对应的HttpHandler来处理该请求。除此之外,请求的URL还会携带相应参数,该中间件在进行路由解析过程中还会根据生成相应的路由参数提供给处理该请求的Handler。为了让读者朋友们对实现在RouterMiddleware的路由功能具有一个大体的认识,我们照例先来演示几个简单的实例。[本文已经同步到《ASP.NET Core框架揭秘》之中]

目录
一、注册请求路径与HttpHandler之间的映射
二、设置内联约束
三、为路由参数设置默认值
四、特殊的路由参数

一、注册请求路径与HttpHandler之间的映射

ASP.NET Core针对请求的处理总是在一个通过HttpContext对象表示的上下文中进行,所以上面我们所说的HttpHandler从编程的角度来讲体现为一个RequestDelegate的委托对象,因此所谓的“路由注册”就是注册一组具有相同默认的请求路径与对应RequestDelegate之间的映射关系。接下来我们就同一个简单的实例来演示这样的映射关系是如何通过注册RouterMiddleware中间件的方式来完成的。

我们演示的这个ASP.NET Core应用是一个简易版的天气预报站点。如果用户希望获取某个城市在未来N天之内的天气信息,他可以直接利用浏览器发送一个GET请求并将对应城市(采用电话区号表示)和天数设置在URL中。如下图所示,为了得到成都未来两天的天气信息,我们发送请求采用的路径为“weather/028/2”。对于路径“weather/0512/4”的请求,返回的自然就是苏州未来4天的添加信息。

为了实现这个简单的应用,我们定义如下一个名为WeatherReport的类型表示某个城市在某段时间范围类的天气。如下面的代码片段所示,我们定义了另一个名为WeatherInfo的类型来表示具体某一天的天气。简单起见,我们让这个WeatherInfo对象只携带基本添加状况和气温区间的信息。当我们创建一个WeatherReport对象的时候,我们会随机生成这些天气信息。

   1: public class WeatherReport
   2: {
   3:     private static string[]     _conditions = new string[] { "晴", "多云", "小雨" };
   4:     private static Random       _random = new Random();
   5:  
   6:     public string                                 City { get; }
   7:     public IDictionary<DateTime, WeatherInfo>     WeatherInfos { get; }
   8:  
   9:     public WeatherReport(string city, int days)
  10:     {
  11:         this.City = city;
  12:         this.WeatherInfos = new Dictionary<DateTime, WeatherInfo>();
  13:         for (int i = 0; i < days; i++)
  14:         {
  15:             this.WeatherInfos[DateTime.Today.AddDays(i + 1)] = new WeatherInfo
  16:             {
  17:                 Condition         = _conditions[_random.Next(0, 2)],
  18:                 HighTemperature   = _random.Next(20, 30),
  19:                 LowTemperature    = _random.Next(10, 20)
  20:             };
  21:         }
  22:     }
  23:  
  24:     public WeatherReport(string city, DateTime date)
  25:     {
  26:         this.City = city;
  27:         this.WeatherInfos = new Dictionary<DateTime, WeatherInfo>
  28:         {
  29:             [date] = new WeatherInfo
  30:             {
  31:                 Condition          = _conditions[_random.Next(0, 2)],
  32:                 HighTemperature    = _random.Next(20, 30),
  33:                 LowTemperature     = _random.Next(10, 20)
  34:             }
  35:         };
  36:     }
  37:  
  38:     public class WeatherInfo
  39:     {
  40:         public string Condition { get; set; }
  41:         public double HighTemperature { get; set; }
  42:         public double LowTemperature { get; set; }
  43:     }
  44: }

我们说最终用于处理请求的HttpHandler最终体现为一个类型为RequestDelegate的委托对象,为此我们定义了如下一个与这个委托类型具有一致声明的方法WeatherForecast来处理针对天气的请求。如下面的代码片段所示,我们在这个方法中直接调用HttpContext的扩展方法GetRouteData得到RouterMiddleware中间件在路由解析过程中得到的路由参数。这个GetRouteData方法返回的是一个具有字典结构的对象,它的Key和Value分别代表路由参数的名称和值,我们通过预先定义的参数名(“city”和“days”)得到目标城市和预报天数。

   1: public class Program
   2: {
   3:     private static Dictionary<string, string> _cities = new Dictionary<string, string>
   4:     {
   5:         ["010"]  = "北京",
   6:         ["028"]  = "成都",
   7:         ["0512"] = "苏州"
   8:     };
   9:  
  10:     public static async Task WeatherForecast(HttpContext context)
  11:     {
  12:         string city = (string)context.GetRouteData().Values["city"]; 
  13:         city = _cities[city];
  14:         int days = int.Parse(context.GetRouteData().Values["days"].ToString());
  15:         WeatherReport report = new WeatherReport(city, days);
  16:  
  17:         context.Response.ContentType = "text/html";
  18:         await context.Response.WriteAsync("<html><head><title>Weather</title></head><body>");
  19:         await context.Response.WriteAsync($"<h3>{city}</h3>");
  20:         foreach (var it in report.WeatherInfos)
  21:         {
  22:             await context.Response.WriteAsync($"{it.Key.ToString("yyyy-MM-dd")}:");
  23:             await context.Response.WriteAsync($"{it.Value.Condition}({it.Value.LowTemperature}℃ ~ {it.Value.HighTemperature}℃)<br/><br/>");
  24:         }
  25:         await context.Response.WriteAsync("</body></html>");
  26:     }
  27:     …
  28: }

有了这两个核心参数之后,我们据此生成一个WeatherReport对象,并将它携带的天气信息以一个HTML文档的形式响应给客户端,图1所示就是这个HTML文档在浏览器上的呈现效果。由于目标城市最初以电话区号的形式体现,在呈现天气信息的过程中我们还会根据区号获取具体城市名称,简单起见,我们利用一个简单的字典来保存区号和城市之间的关系,并且只存储了三个城市而已。

接下来我们来完成所需的路由注册工作,实际上就是注册RouterMiddleware中间件。由于这各中间件定义在“Microsoft.AspNetCore.Routing”这个NuGet包中,所以我们需要添加对应的依赖。如下面的代码片段所示,针对RouterMiddleware中间件的注册实现在ApplicationBuilder的扩展方法UseRouter中。由于RouterMiddleware中间件在进行路由解析的过程中需要使用到一些服务,我们调用WebHostBuilder的ConfigureServices方法注册的就是这些服务。具体来说,这些与路由相关的服务是通过调用ServiceCollection的扩展方法AddRouting实现的。

   1: public class Program
   2: {    
   3:     public static void Main()
   4:     {
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .ConfigureServices(svcs => svcs.AddRouting())
   8:             .Configure(app => app.UseRouter(builder => builder.MapGet("weather/{city}/{days}", WeatherForecast)))
   9:             .Build()
  10:             .Run();
  11:     }
  12:     …
  13: }

RouterMiddleware中间件针对路由的解析依赖于一个名为Router的对象,对应的接口为IRouter。我们在程序中会先根据ApplicationBuilder对象创建一个RouteBuilder对象,并利用后者来创建这个Router。我们说路由注册从本质上体现为注册某种URL模式与一个RequestDelegate对象之间的映射,这个映射关系的建立是通过调用RouteBuilder的MapGet方法的调用。MapGet方法具有两个参数,第一个参数代表映射的URL模板,后者是处理请求的RequestDelegate对象。我们指定的URL模板为“weather/{city}/{days}”,其中携带两个路由参数({city}和{days}),我们知道它代表获取天气预报的目标城市和天数。由于针对天气请求的处理实现在我们定义的WeatherReport方法中,我们将指向这个方法的RequestDelegate对象作为第二个参数。

二、设置内联约束

在上面进行路由注册的实例中,我们在注册的URL模板中定义了两个参数({city}和{days})来分别代表获取天气预报的目标城市对应的区号和天数。区号应该具有一定的格式(以零开始的3-4位数字),而天数除了必须是一个整数之外,还应该具有一定的范围。由于我们在注册的时候并没有为这个两个路由参数的取值做任何的约束,所以请求URL携带的任何字符都是有效的。而处理请求的WeatherForecast方法也并没有对提取的数据做任何的验证,所以在执行过程中会直接抛出异常。如下图所示,由于请求URL(“/weather/0512/iv”)指定了天数不合法,所有客户端接收到一个状态为“500 Internal Server Error”的响应。

为了确保路由参数数值的有效性,我们在进行路由注册的时候可以采用内联(Inline)的方式直接将相应的约束规则定义在路由模板中。ASP.NET Core针对我们常用的验证规则定义了相应的约束表达式,我们可以根据需要为某个路由参数指定一个或者多个约束表达式。

如下面的代码片段所示,为了确保URL携带的是合法的区号,我们为路由参数{city}应用了一个针对正则表达式的约束(:regex(^0[1-9]{{2,3}}$))。由于路由模板在被解析的时候会将“{…}”这样的字符理解为路由参数,如果约束表达式需要使用“{}”字符(比如正则表达式“^0[1-9]{2,3}$)”),需要采用“{{}}”进行转义。至于另一个路由参数{days}则应用了两个约束,第一个是针对数据类型的约束(:int),它要求参数值必须是一个整数。另一个是针对区间的约束(:range(1,4)),意味着我们的应用最多只提供未来4天的天气。

   1: string template = @"weather/{city:regex(^0\d{{2,3}}$)}/{days:int:range(1,4)}";
   2: new WebHostBuilder()
   3:     .UseKestrel()
   4:     .ConfigureServices(svcs => svcs.AddRouting())
   5:     .Configure(app => app.UseRouter(builder=> builder.MapGet(template, WeatherForecast)))
   6:     .Build()
   7:     .Run();

如果我们在注册路由的时候应用了约束,那么当RouterMiddleware中间件在进行路由解析的时候除了要求请求路径必须与路由模板具有相同的模式,同时还要求携带的数据满足对应路由参数的约束条件。如果不能同时满足这两个条件,RouterMiddleware中间件将无法选择一个RequestDelegate对象来处理当前请求,在此情况下它将直接将请求递交给后续的中间件进行处理。对于我们演示的这个实例来说,如果我们提供一个不合法的区号(1014)和预报天数(5),客户端都将得到一个状态码为“404 Not Found”的响应。

三、为路由参数设置默认值

路由注册时提供的路由模板(比如“Weather/{city}/{days}”)可以包含静态的字符(比如“weather”),也可以包括动态的参数(比如{city}和{days}),我们将它们成为路由参数。并非每个路由参数都是必需的(要求路由参数的值必需存在请求路径中),有的路由参数是可以缺省的。还是以上面演示的实例来说,我们可以采用如下的方式在路由参数名后面添加一个问号(“?”),原本必需的路由参数变成了可以缺省的。可缺省的路由参数只能出现在路由模板尾部,这个应该不难理解。

   1: string template = "weather/{city?}/{days?}";
   2: new WebHostBuilder()
   3:     .UseKestrel()
   4:     .ConfigureServices(svcs => svcs.AddRouting())
   5:     .Configure(app => app.UseRouter(builder=> builder.MapGet(template, WeatherForecast)))
   6:     .Build()
   7:     .Run();

既然可以路由变量占据的部分路径是可以缺省的,那么意味即使请求的URL不具有对应的内容(比如“weather”和“weather/010”),在进行路由解析的时候同样该请求与路由规则相匹配,但是在最终的路由参数字典中将找不到它们。由于表示目标城市和预测天数的两个路由参数都是可缺省的,我们需要对处理请求的WeatherForecast方法做作相应的改动。下面的代码片段表明如果请求URL为显式提供对应参数的数据,它们的默认值分别为“010”(北京)和4(天),也就是说应用默认提供北京地区未来四天的天气。

   1: public static async Task WeatherForecast(HttpContext context)
   2: {
   3:     object rawCity;
   4:     object rawDays;
   5:     var values = context.GetRouteData().Values;
   6:     string city = values.TryGetValue("city", out rawCity) ? rawCity.ToString() : "010";
   7:     int days = values.TryGetValue("days", out rawDays) ? int.Parse(rawDays.ToString()) : 4;     
   8:                    
   9:     city = _cities[city];
  10:     WeatherReport report = new WeatherReport(city, days);
  11:     …
  12: }

针对上述的改动,如果希望获取北京未来四天的天气状况,我们可以采用如下图所示的三种URL(“weather”和“weather/010”和“weather/010/4”),它们都是完全等效的。

上面我们的程序相当于是在进行请求处理的时候给予了可缺省路由参数一个默认值,实际上路由参数默认值得设置还具有一种更简单的方式,那就是按照如下所示的方式直接将默认值定义在路由模板中。如果采用这样的路由注册方式,我们针对WeatherForecast方法的改动就完全没有必要了。

   1: string template = "weather/{city=010}/{days=4}";
   2: new WebHostBuilder()
   3:     .UseKestrel()
   4:     .ConfigureServices(svcs => svcs.AddRouting())
   5:     .Configure(app =>app.UseRouter(builder=>builder.MapGet(template, WeatherForecast)))
   6:     .Build()
   7:     .Run();

四、特殊的路由参数

一个URL可以通过分隔符“/”划分为多个路径分段(Segment),路由模板中定义的路由参数一般来说会占据某个独立的分段(比如“weather/{city}/{days}”)。不过也有特例,我们即可以在一个单独的路径分段中定义多个路由参数,同样也可以让一个路由参数跨越对个连续的路径分段。

我们先来介绍在一个独立的路径分段中定义多个路由参数的情况。同样以我们演示的获取天气预报的URL为例,假设我们设计一种URL来获取某个城市某一天的天气信息,比如“/weather/010/2016.11.11”这样一个URL可以获取北京地区在2016年双11那天的天气,那么路由模板为“/weather/{city}/{year}.{month}.{day}”。

   1: string tempalte = "weather/{city}/{year}.{month}.{day}";
   2: new WebHostBuilder()
   3:     .UseKestrel()
   4:     .ConfigureServices(svcs => svcs.AddRouting())
   5:     .Configure(app => app.UseRouter(builder=>builder.MapGet(tempalte, WeatherForecast)))
   6:     .Build()
   7:     .Run();
   8:  
   9: public static async Task WeatherForecast(HttpContext context)
  10: {
  11:     var values     = context.GetRouteData().Values;
  12:     string city    = values["city"].ToString();
  13:     city           = _cities[city];
  14:     int year       = int.Parse(values["year"].ToString());
  15:     int month      = int.Parse(values["month"].ToString());
  16:     int day        = int.Parse(values["day"].ToString());
  17:  
  18:     WeatherReport report = new WeatherReport(city, new DateTime(year,month,day));
  19:     …
  20: }

由于URL采用了新的设计,所以我们按照如上的形式对相关的程序进行了相应的修改。现在我们采用匹配的URL(比如“/weather/010/2016.11.11”)就可以获取到某个城市指定日期的天气。

对于上面设计的这个URL来说,我们采用“.”作为日期分隔符,如果我们采用“/”作为日期分隔符(比如“2016/11/11”),这个路由默认应该如何定义呢?由于“/”同时也是URL得路径分隔符,如果表示日期的路由变量也采用相同的分隔符,意味着同一个路由参数跨越了多个路径分段,我们只能定义“通配符”路由参数的形式来达到这个目的。通配符路由参数采用“{*variable}”这样的形式,星号(“*”)表示路径“余下的部分”,所以这样的路由参数只能出现在模板的尾端。对我们的实例来说,路由模板可以定义成“/weather/{city}/{*date}”。

   1: new WebHostBuilder()
   2:     .UseKestrel()
   3:     .ConfigureServices(svcs => svcs.AddRouting())
   4:     .Configure(app => {
   5:         string tempalte = "weather/{city}/{*date}";
   6:         IRouter router  = new RouteBuilder(app).MapGet(tempalte, WeatherForecast).Build();
   7:         app.UseRouter(router);
   8:     })
   9:     .Build()
  10:     .Run();
  11:  
  12: public static async Task WeatherForecast(HttpContext context)
  13: {
  14:     var values      = context.GetRouteData().Values;
  15:     string city     = values["city"].ToString();
  16:     city            = _cities[city];
  17:     DateTime date   = DateTime.ParseExact(values["date"].ToString(), "yyyy/MM/dd", 
  18:     CultureInfo.InvariantCulture);
  19:     WeatherReport report = new WeatherReport(city, date);
  20:     …
  21: }

我们可以对程序做如上的修改来使用新的URL模板(“/weather/{city}/{*date}”)。这样为了得到如上图所示的北京在2016年11月11日的天气,请求的URL可以替换成“/weather/010/2016/11/11”。

作者:蒋金楠 
微信公众账号:大内老A
微博:www.weibo.com/artech

注册URL模式与HttpHandler的映射关系的更多相关文章

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

    ASP.NET Core的路由是通过一个类型为RouterMiddleware的中间件来实现的.如果我们将最终处理HTTP请求的组件称为HttpHandler,那么RouterMiddleware中间 ...

  2. Repository模式--采用EF Fluent API使用EntityTypeConfiguration分文件配置Model映射关系

    EF中类EntityTypeConfiguration是一个很有用的类,在nopCommerence中就使用这个类来分文件分文件配置Model映射关系.今天我就来谈谈Repository模式在Enti ...

  3. Hibernate4.2.4入门(二)——一对多的映射关系

    一.前言 前面我们已经学过hibernate的基础,学会增删改查简单的操作,然而我们数据库中存在着1对多,多对1,多对多的关系,hibernate又是基于ORM基础上的开源框架,可以让我们不用去编写S ...

  4. Hibernate关联映射关系

    Hibernate关联映射关系 一.双向一对多关联映射关系:当类与类之间建立了关联,就可以方便的从一个对象导航到另一个或另一组与它关联的对象(一对多双向关联和多对一双向关联是完全一样的) 1.1创建实 ...

  5. PHP 设计模式 笔记与总结(6)基础设计模式:工厂模式、单例模式和注册树模式

    三种基础设计模式(所有面向对象设计模式中最常见的三种): ① 工厂模式:使用工厂方法或者类生成对象,而不是在代码中直接new 在 Common 目录下新建 Factory.php: <?php ...

  6. (八)Hibernate 映射关系

    所有项目导入对应的hibernate的jar包.mysql的jar包和添加每次都需要用到的HibernateUtil.java 第一节:Hibernate 一对一映射关系实现 1,按照主键映射: 2, ...

  7. SSH框架之Hibernate(1)——映射关系

    ORM的实现思想就是将关系数据库中表的数据映射成对象.以对象的形式展现,这样开发者就能够把对数据库的操作转化为对这些对象的操作.Hibernate正是实现了这样的思想,达到了方便开发者以面向对象的思想 ...

  8. EntityFramework Core映射关系详解

    前言 Hello,开始回归开始每周更新一到两篇博客,本节我们回归下EF Core基础,来讲述EF Core中到底是如何映射的,废话少说,我们开始. One-Many Relationship(一对多关 ...

  9. Django创建模板、URL模式、创建视图函数

    1.在应用目录下创建模板(templates目录) 在模板目录下创建archive.html <!DOCTYPE html> <html lang="en"> ...

随机推荐

  1. redis数据类型

    Redis 数据类型 Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合). String(字符串) st ...

  2. Linux下怎么查看当前系统的版本

    Linux下怎么查看当前系统的版本:   uname -r 功能说明:uname用来获取电脑和操作系统的相关信息. 语 法:uname [-amnrsvpio][--help][--version] ...

  3. JMS发布/订阅消息传送例子

    前言 基于上篇文章"基于Tomcat + JNDI + ActiveMQ实现JMS的点对点消息传送"很容易就可以编写一个发布/订阅消息传送例子,相关环境准备与该篇文章基本类似,主要 ...

  4. 网络编程3--毕向东java基础教程视频学习笔记

    Day24 01 TCP上传图片02 客户端并发上传图片03 客户端并发登录04 浏览器客户端-自定义服务端05 浏览器客户端-Tomcat服务端 01 TCP上传图片 import java.net ...

  5. 每日Scrum(4)

    今天是冲刺第4天,小组也没有做什么,大家都忙着找大二的学弟学妹来点评来支持我们的软件. 遇到的问题主要是如何劝说学弟学妹选择我们的软件然后继续往下做.

  6. 旧项目如何切换到Entity Framework Code First

    Entity Framework Code First固然是好东西,然而如果是已经存在的旧有项目,如何简单方便的使用切换呢? 这里介绍一个VS的插件Entity Framework Power Too ...

  7. Windows Phone 8.0 SDK Update(10322) Released

    昨天微软低调发布了WP 8 SDK的更新,甚至在Windows Phone Developer Blog上都没有提及. 从开发者的角度来看,此次更新的确没有太多需要关注的地方,因为没有添加新的API和 ...

  8. android中基于HTML模板的方式嵌入SWF

    继上一篇 利用webview实现在andorid中嵌入swf 这篇继续说说通过html模板的方式来嵌入SWF,这样做的好处最直观的就是可以把html,swf和android代码串起来,交互操作很方便( ...

  9. Java并发之BlockingQueue 阻塞队列(ArrayBlockingQueue、LinkedBlockingQueue、DelayQueue、PriorityBlockingQueue、SynchronousQueue)

    package com.thread.test.thread; import java.util.Random; import java.util.concurrent.*; /** * Create ...

  10. JS实现别踩白块小游戏

    最近有朋友找我用JS帮忙仿做一个别踩白块的小游戏程序,但他给的源代码较麻烦,而且没有注释,理解起来很无力,我就以自己的想法自己做了这个小游戏,主要是应用JS对DOM和数组的操作. 程序思路:如图:将游 ...