ASP.NET Core路由中间件[1]: 终结点与URL的映射
目录
一、路由注册
二、设置内联约束
三、默认路由参数
四、特殊的路由参数
借助路由系统提供的请求URL模式与对应终结点(Endpoint)之间的映射关系,我们可以将具有相同URL模式的请求分发给应用的终结点进行处理。ASP.NET Core的路由是通过EndpointRoutingMiddleware和EndpointMiddleware这两个中间件协作完成的,它们在ASP.NET Core平台上具有举足轻重的地位,因为ASP.NET Core MVC框架就建立在这个中间件之上。可以将一个ASP.NET Core应用视为一组终结点的组合,所谓的终结点可以理解为能够通过HTTP请求的形式访问的远程服务。每个终结点通过RequestDelegate对象来处理路由过来的请求。ASP.NET Core的路由是通过EndpointRoutingMiddleware和EndpointMiddleware这两个中间件来实现的,这两个中间件类型都定义在NuGet包“Microsoft.AspNetCore.Routing”中。为了使读者对实现在RouterMiddleware的路由功能有一个大体的认识,下面先演示几个简单的实例。
一、路由注册
我们演示的这个ASP.NET Core应用是一个简易版的天气预报站点。如果用户希望获取某个城市在未来N天之内的天气信息,他可以直接利用浏览器发送一个GET请求并将对应城市(采用电话区号表示)和天数设置在URL中。如下图所示,为了得到成都未来两天的天气信息,我们将发送请求的路径设置为“weather/028/2”。对于采用路径“weather/0512/4”的请求,返回的自然就是苏州未来4天的天气信息。
为了开发这个简单的应用,我们定义了如下所示的WeatherReport类型,表示某个城市在某段时间范围内的天气。如下面的代码片段所示,我们还定义了另一个WeatherInfo类型,表示具体某一天的天气。简单起见,我们让WeatherInfo对象只携带基本天气状况和气温区间的信息。创建一个WeatherReport对象时,我们会随机生成这些天气信息。
public class WeatherReport
{
private static string[] _conditions = new string[] { "晴", "多云", "小雨" };
private static Random _random = new Random(); public string City { get; }
public IDictionary<DateTime, WeatherInfo> WeatherInfos { get; } public WeatherReport(string city, int days)
{
City = city;
WeatherInfos = new Dictionary<DateTime, WeatherInfo>();
for (int i = 0; i < days; i++)
{
WeatherInfos[DateTime.Today.AddDays(i + 1)] = new WeatherInfo
{
Condition = _conditions[_random.Next(0, 2)],
HighTemperature = _random.Next(20, 30),
LowTemperature = _random.Next(10, 20)
};
}
} public WeatherReport(string city, DateTime date)
{
City = city;
WeatherInfos = new Dictionary<DateTime, WeatherInfo>
{
[date] = new WeatherInfo
{
Condition = _conditions[_random.Next(0, 2)],
HighTemperature = _random.Next(20, 30),
LowTemperature = _random.Next(10, 20)
}
};
} public class WeatherInfo
{
public string Condition { get; set; }
public double HighTemperature { get; set; }
public double LowTemperature { get; set; }
}
}
由于用于处理请求的处理器最终体现为一个RequestDelegate对象,所以我们定义了如下一个与这个委托类型具有一致声明的WeatherForecast方法来处理对应的请求。如下面的代码片段所示,我们在这个方法中直接调用HttpContext的GetRouteData扩展方法提取RoutingMiddleware中间件在路由解析过程中设置的路由参数。GetRouteData扩展方法返回的是一个具有字典结构的对象,它的Key和Value分别代表路由参数的名称与值,通过预先定义的参数名(city和days)可以得到目标城市和预报天数。
public class Program
{
private static Dictionary<string, string> _cities = new Dictionary<string, string>
{
["010"] = "北京",
["028"] = "成都",
["0512"] = "苏州"
}; public static async Task WeatherForecast(HttpContext context)
{
var city = (string)context.GetRouteData().Values["city"];
city = _cities[city];
int days = int.Parse(context.GetRouteData().Values["days"].ToString());
var report = new WeatherReport(city, days);
await RendWeatherAsync(context, report);
} private static async Task RendWeatherAsync(HttpContext context, WeatherReport report)
{
context.Response.ContentType = "text/html;charset=utf-8";
await context.Response.WriteAsync("<html><head><title>Weather</title></head><body>");
await context.Response.WriteAsync($"<h3>{report.city}</h3>");
foreach (var it in report.WeatherInfos)
{
await context.Response.WriteAsync($"{it.Key.ToString("yyyy-MM-dd")}:");
await context.Response.WriteAsync($"{it.Value.Condition}({ it.Value.LowTemperature}℃ ~ { it.Value.HighTemperature}℃)< br />< br /> ");
}
await context.Response.WriteAsync("</body></html>");
}
...
}
有了这两个核心参数之后,我们可以据此生成一个WeatherReport对象,并将它携带的天气信息以一个HTML文档的形式响应给客户端,图15-1就是这个HTML文档在浏览器上的呈现效果。由于目标城市最初以电话区号的形式体现,所以在呈现天气信息的过程中我们还会根据区号获取具体城市的名称。简单起见,我们利用一个简单的字典来维护区号和城市之间的关系,并且只存储了3个城市而已。
下面完成所需的路由注册工作。如下面的代码片段所示,我们调用IApplicationBuilder的UseRouting方法和UseEndpoints方法分别完成针对EndpointRoutingMiddleware与EndpointMiddleware这两个终结点的注册。由于它们在进行路由解析过程中需要使用一些服务,所以可以调用IServiceCollection的AddRouting扩展方法来对它们进行注册。
public class Program
{
public static void Main()
{
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder => builder
.ConfigureServices(svcs => svcs.AddRouting())
.Configure(app => app
.UseRouting()
.UseEndpoints(endpoints=> endpoints.MapGet("weather/{city}/{days}", WeatherForecast))))
.Build()
.Run();
}
}
UseEndpoints方法提供了一个Action<IEndpointRouteBuilder>类型的参数,我们利用这个参数调用IEndpointRouteBuilder的MapGet方法提供了一个路由模板与对应处理器之间的映射。我们指定的路径模板为“weather/{city}/{days}”,其中携带两个路由参数({city}和{days}),分别代表获取天气预报的目标城市和天数。由于针对天气请求的处理实现在WeatherForecast方法中,所以将指向这个方法的RequestDelegate对象作为第二个参数。MapGet的后缀“Get”表示HTTP方法,这意味着与指定路由模板的模式相匹配的GET请求才会被路由到WeatherForecast方法对应的终结点。
二、设置内联约束
上面的演示实例注册的路由模板中定义了两个参数({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}}$))。由于路由模板在被解析时会将{value}这样的字符理解为路由参数,如果约束表达式需要使用字符“{}”(如正则表达式^0[1-9]{2,3}$)),就需要采用“{{}}”进行转义。而路由参数{days}则应用了两个约束:第一个是针对数据类型的约束(:int),它要求参数值必须是一个整数;第二个是针对区间的约束(:range(1,4)),意味着我们的应用最多只提供未来4天的天气。
public class Program
{
public static void Main()
{
var template = @"weather/{city:regex(^0\d{{2,3}}$)}/{days:int:range(1,4)}";
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder => builder
.ConfigureServices(svcs => svcs.AddRouting())
.Configure(app => app
.UseRouting()
.UseEndpoints(routes => routes.MapGet(template, WeatherForecast))))
.Build()
.Run();
}
...
}
如果在注册路由时应用了约束,那么RoutingMiddleware中间件在进行路由解析时除了要求请求路径必须与路由模板具有相同的模式,还要求携带的数据满足对应路由参数的约束条件。如果不能同时满足这两个条件,RoutingMiddleware中间件将无法选择一个终结点来处理当前请求,在此情况下它会将请求直接递交给后续中间件进行处理。对于我们演示的这个实例来说,如果提供的是一个不合法的区号(1024)和预报天数(5),那么客户端都将得到下图所示的状态码为“404 Not Found”的响应。
三、默认路由参数
路由注册时提供的路由模板(如“weather/{city}/{days}”)可以包含静态的字符(如weather),也可以包含动态的参数(如{city}和{days}),我们将后者称为路由参数。并非每个路由参数都是必需的,有的路由参数是默认的。还是以上面演示的实例来说,我们可以采用如下方式在路由参数名后面添加一个问号(?)将原本必需的路由参数变成可以默认的。默认的路由参数只能出现在路由模板尾部,这个应该不难理解。
public class Program
{
public static void Main()
{
var template = "weather/{city?}/{days?}";
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder => builder
.ConfigureServices(svcs => svcs.AddRouting())
.Configure(app => app
.UseRouting()
.UseEndpoints(routes => routes.MapGet(template, WeatherForecast))))
.Build()
.Run();
}
...
}
既然路由变量占据的部分路径是可以默认的,那么即使请求的URL不具有对应的内容(如“weather”和“weather/010”),它与路由规则也是匹配的,但此时在路由参数字典中是找不到它们的。由于表示目标城市和预测天数的两个路由参数都是默认的,所以需要对处理请求的WeatherForecast方法做相应的改动。下面的代码片段表明:如果请求URL为了显式提供对应参数的数据,那么它们的默认值分别为010(北京)和4(天),也就是说,应用默认提供北京未来4天的天气。
public class Program
{
public static async Task WeatherForecast(HttpContext context)
{
var routeValues = context.GetRouteData().Values;
var city = routeValues.TryGetValue("city", out var v1)
? (string)v1
: "010";
city = _cities[city];
var days = routeValues.TryGetValue("days", out var v2)
? int.Parse(v2.ToString())
: 4;
var report = new WeatherReport(city, days);
await RendWeatherAsync(context, report);
}
...
}
针对上述改动,如果希望获取北京未来4天的天气状况,我们可以采用下图所示的3种URL(“weather”、“weather/010”和“weather/010/4”),它们是完全等效的。
上面的程序相当于在进行请求处理时给予了默认路由参数一个默认值,实际上,路由参数默认值的设置还有一种更简单的方式,那就是按照如下所示的方式直接将默认值定义在路由模板中。如果采用这样的路由注册方式,针对WeatherForecast方法的改动就完全没有必要。
public class Program
{
public static void Main()
{
var template = "weather/{city=010}/{days=4}";
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder => builder
.ConfigureServices(svcs => svcs.AddRouting())
.Configure(app => app
.UseRouting()
.UseEndpoints(routes => routes.MapGet(template, WeatherForecast))))
.Build()
.Run();
}
...
}
四、特殊的路由参数
一个URL可以通过分隔符“/”划分为多个路径分段(Segment),路由模板中定义的路由参数一般来说会占据某个独立的分段(如“weather/{city}/{days}”)。但也有例外情况,我们既可以在一个单独的路径分段中定义多个路由参数,也可以让一个路由参数跨越多个连续的路径分段。
下面先介绍在一个独立的路径分段中定义多个路由参数的情况。同样以前面演示的获取天气预报的路径为例,假设设计一种路径模式来获取某个城市某一天的天气信息,如“/weather/010/2019.11.11”这样一个URL可以获取北京在2019年11月11日的天气,那么路由模板为“/weather/{city}/{year}.{month}.{day}”。
public class Program
{
public static void Main()
{
var template = "weather/{city}/{year}.{month}.{day}";
Host.CreateDefaultBuilder().ConfigureWebHostDefaults(builder => builder
.ConfigureServices(svcs => svcs.AddRouting())
.Configure(app => app.UseRouter(builder => builder.MapGet(template, WeatherForecast))))
.Build()
.Run();
} public static async Task WeatherForecast(HttpContext context)
{
var values = context.GetRouteData().Values;
var city = values["city"].ToString();
city = _cities[city];
int year = int.Parse(values["year"].ToString());
int month = int.Parse(values["month"].ToString());
int day = int.Parse(values["day"].ToString());
var report = new WeatherReport(city, new DateTime(year, month, day));
await RendWeatherAsync(context, report);
}
...
}
由于URL采用了新的设计,所以我们按照如上形式对相关程序进行了相应的修改。现在我们采用“/weather/{city}/{yyyy}.{mm}.{dd}”这样的URL,就可以获取某个城市指定日期的天气。如下图所示,我们采用请求路径“/weather/010/2019.11.11”可以获取北京在2019年11月11日的天气。
对于上面设计的这个URL来说,我们采用“.”作为日期分隔符,如果采用“/”作为日期分隔符(如2019/11/11),这个路由默认应该如何定义?由于“/”也是路径分隔符,如果表示日期的路由变量也采用相同的分隔符,就意味着同一个路由参数跨越了多个路径分段,我们只能采用定义“通配符”的形式来达到这个目的。通配符路由参数采用{*variable}或者{**variable}的形式,星号(*)表示路径“余下的部分”,所以这样的路由参数只能出现在模板的尾端。对我们的实例来说,路由模板可以定义成“/weather/{city}/{*date}”。
public class Program
{
public static void Main()
{
var template = "weather/{city}/{*date}";
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder => builder
.ConfigureServices(svcs => svcs.AddRouting())
.Configure(app => app
.UseRouting()
.UseEndpoints(routes => routes.MapGet(template, WeatherForecast))))
.Build()
.Run();
} public static async Task WeatherForecast(HttpContext context)
{
var values = context.GetRouteData().Values;
var city = values["city"].ToString();
city = _cities[city];
var date = DateTime.ParseExact(values["date"].ToString(), "yyyy/MM/dd", CultureInfo.InvariantCulture);
var report = new WeatherReport(city, date);
await RendWeatherAsync(context, report);
}
...
}
我们可以对程序做如上修改来使用新的URL模板(“/weather/{city}/{*date}”)。为了得到北京在2019年11月11日的天气,请求的URL可以替换成“/weather/010/2019/11/11”,返回的天气信息如下图所示。
ASP.NET Core路由中间件[1]: 终结点与URL的映射的更多相关文章
- ASP.NET Core路由中间件[3]: 终结点(Endpoint)
到目前为止,ASP.NET Core提供了两种不同的路由解决方案.传统的路由系统以IRouter对象为核心,我们姑且将其称为IRouter路由.本章介绍的是最早发布于ASP.NET Core 2.2中 ...
- ASP.NET Core路由中间件[2]: 路由模式
一个Web应用本质上体现为一组终结点的集合.终结点则体现为一个暴露在网络中可供外界采用HTTP协议调用的服务,路由的作用就是建立一个请求URL模式与对应终结点之间的映射关系.借助这个映射关系,客户端可 ...
- asp.net core mvc 中间件之路由
asp.net core mvc 中间件之路由 路由中间件 首先看路由中间件的源码 先用httpContext实例化一个路由上下文,然后把中间件接收到的路由添加到路由上下文的路由集合 然后把路由上下文 ...
- ASP.NET Core:中间件
一.什么是中间件 我们都知道,任何的一个web框架都是把http请求封装成一个管道,每一次的请求都是经过管道的一系列操作,最终才会到达我们写的代码中.而中间件就是用于组成应用程序管道来处理请求和响应的 ...
- ASP.NET Core 路由 - ASP.NET Core 基础教程 - 简单教程,简单编程
原文:ASP.NET Core 路由 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core 路由 前两章节中,我们提到 ASP.NET Core 支持 MVC 开发 ...
- 如何传递参数给ASP.NET Core的中间件(Middleware)
问题描述 当我们在ASP.NET Core中定义和使用中间件(Middleware)的时候,有什么好的办法可以给中间件传参数吗? 解决方案 在ASP.NET Core项目中添加一个POCO类来传递参数 ...
- asp.net core mvc 中间件之WebpackDevMiddleware
asp.net core mvc 中间件之WebpackDevMiddleware WebpackDevMiddleware中间件主要用于开发SPA应用,启用Webpack,增强网页开发体验.好吧,你 ...
- 如何在ASP.NET Core自定义中间件中读取Request.Body和Response.Body的内容?
原文:如何在ASP.NET Core自定义中间件中读取Request.Body和Response.Body的内容? 文章名称: 如何在ASP.NET Core自定义中间件读取Request.Body和 ...
- 构建可读性更高的 ASP.NET Core 路由
原文:构建可读性更高的 ASP.NET Core 路由 一.前言 不知你在平时上网时有没有注意到,绝大多数网站的 URL 地址都是小写的英文字母,而我们使用 .NET/.NET Core MVC 开发 ...
随机推荐
- 大厂是如何用DevCloud流水线实现自动化部署Web应用的?
DevUI是一支兼具设计视角和工程视角的团队,服务于华为云DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师. 官方网站:devui.design Ng组件库:ng-devui(欢 ...
- Mysql 存储过程(变量、定义条件、处理程序、光标、流程控制构造)
最近由于有同事编写了存储函数,需要进行验证,但是对存储过程一直不是很了解,所以抽时间了解了一下存储过程的基本语法.本篇文章主要包括介绍了存储过程的5个小语法:declare语句的变量.定义条件.处理程 ...
- kubeadm 的工作原理
kubeadm 的工作原理 作者:张首富 时间:2020-06-04 w x:y18163201 相信使用二进制部署过 k8s 集群的同学们都知道,二进制部署集群太困难了,有点基础的人部署起来还有成功 ...
- 「IOI2017」接线 的另类做法
看到这题,我的第一反应是:这就是一个费用流模型?用模拟费用流的方法? 这应该是可以的,但是我忘记了怎么模拟费用流了IOI不可能考模拟费用流.于是我就想了另外一个方法. 首先我们考虑模拟费用流的模型如下 ...
- "利用python进行数据分析"学习记录01
"利用python进行数据分析"学习记录 --day01 08/02 与书相关的资料在 http://github.com/wesm/pydata-book pandas 的2名字 ...
- AcWing 294. 计算重复
暴力 其实这题的暴力就是个模拟.暴力扫一遍 \(conn(s_1, n_1)\),若出现了 \(res\) 个 \(s_2\). 答案就是 \(\lfloor res / n1 \rfloor\). ...
- 算法——移掉K位数字使得数值最小
给定一个以字符串表示的非负整数 num,移除这个数中的 k 位数字,使得剩下的数字最小. leetcode 解题思路:如果这个数的各个位是递增的,那么直接从最后面开始移除一定就是最最小的:如果这个数的 ...
- 云服务器 ECS Linux 安装 VNC Server 实现图形化访问配置说明
阿里云官方公共 Linux 系统镜像,基于性能及通用性等因素考虑,默认没有安装 VNC 服务组件.本文对常见操作系统下的 VNC Server 安装配置进行简要说明. 本文中仅讨论VNC的安装,关于图 ...
- 超详细!使用 LVS 实现负载均衡原理及安装配置详解---转
负载均衡集群是 load balance 集群的简写,翻译成中文就是负载均衡集群.常用的负载均衡开源软件有nginx.lvs.haproxy,商业的硬件负载均衡设备F5.Netscale.这里主要是学 ...
- Vue--子组件相互传参
引用了element做按钮组件 父组件 创建子组件公用的空vue变量,为pubVue,并传给需要互相传参/互相调用方法的两个子组件 <template> <div> <b ...