借助路由系统提供的请求URL模式与对应终结点之间的映射关系,我们可以将具有相同URL模式的请求分发给与之匹配的终结点进行处理。ASP.NET的路由是通过EndpointRoutingMiddleware和EndpointMiddleware这两个中间件协作完成的,它们在ASP.NET平台上具有举足轻重的地位,MVC和gRPC框架,Dapr的Actor和发布订阅编程模式都建立在路由系统之上。Minimal API更是将提升到了前所未有的高度,是我们直接在路由系统基础上定义REST API。

[S2001]注册路由终结点 (源代码

[S2002]以内联方式设置路由参数的约束(源代码

[S2003]定义可缺省的路由参数(源代码

[S2004]为路由参数指定默认值(源代码

[S2005]一个路径分段定义多个路由参数(源代码

[S2006]一个路由参数跨越多个路径分段(源代码

[S2007]主机名绑定(源代码

[S2008]将终结点处理定义为任意类型的委托(源代码

[S2009]IResult 的应用(源代码

[S2001]注册路由终结点

我们演示的这个ASP.NET应用是一个简易版的天气预报站点。服务端利用注册的一个终结点来提供某个城市在未来N天之内的天气信息,对应城市(采用电话区号表示)和天数直接至于请求URL的路径中。如图1所示,为了得到成都未来两天的天气信息,我们将发送请求的路径设置为“weather/028/2”。路径为“weather/0512/4”的请求返回就是苏州未来4天的天气信息(S2001)。

图1 获取天气预报信息

演示程序定义了如下这个WeatherReport记录类型来表示某个城市在某段时间范围内的天气报告。如代码片段所示,某一天的天气体现为一个WeatherInfo记录。简单起见,我们让WeatherInfo记录只携带基本天气状况和气温区间的信息。

public readonly record struct WeatherInfo(string Condition, double HighTemperature, double LowTemperature);
public readonly record struct WeatherReport(string CityCode, string CityName,IDictionary<DateTime, WeatherInfo> WeatherInfos);

我们定义了如下这个工具类型WeatherReportUtility,两个Generate方法会根据指定的城市代码和天数/日期生成一份由WeatherReport对象表示的天气报告。为了将这份报告呈现在网页上,我们定义了另一个RenderAsync方法将指定的WeatherReport转换成HTML,并利用指定的HttpContext上下文将它作为响应内容,具体的HTML内容由AsHtml方法生成。

public static class WeatherReportUtility
{
private static readonly Random _random = new();
private static readonly Dictionary<string, string> _cities = new()
{
["010"] = "北京",
["028"] = "成都",
["0512"] = "苏州"
};
private static readonly string[] _conditions = new string[] { "晴", "多云", "小雨" };
public static WeatherReport Generate(string city, int days)
{
var report = new WeatherReport(city, _cities[city], new Dictionary<DateTime, WeatherInfo>());
for (int i = 0; i < days; i++)
{
report.WeatherInfos[DateTime.Today.AddDays(i + 1)] = new WeatherInfo(_conditions[_random.Next(0, 2)], _random.Next(20, 30), _random.Next(10, 20));
}
return report;
}
public static WeatherReport Generate(string city, DateTime date)
{
var report = new WeatherReport(city, _cities[city], new Dictionary<DateTime, WeatherInfo>());
report.WeatherInfos[date] = new WeatherInfo(_conditions[_random.Next(0, 2)], _random.Next(20, 30), _random.Next(10, 20));
return report;
}
public static Task RenderAsync(HttpContext context, WeatherReport report)
{
context.Response.ContentType = "text/html;charset=utf-8";
return context.Response.WriteAsync(AsHtml(report));
} public static string AsHtml(WeatherReport report)
{
return @$"
<html>
<head><title>Weather</title></head>
<body>
<h3>{report.CityName}</h3>
{AsHtml(report.WeatherInfos)}
</body>
</html>
";
static string AsHtml(IDictionary<DateTime, WeatherInfo> dictionary)
{
var builder = new StringBuilder();
foreach (var kv in dictionary)
{
var date = kv.Key.ToString("yyyy-MM-dd");
var tempFrom = $"{kv.Value.LowTemperature}℃ ";
var tempTo = $"{kv.Value.HighTemperature}℃ ";
builder.Append( $"{date}: {kv.Value.Condition} ({tempFrom}~{tempTo})<br/></br>");
}
return builder.ToString();
}
}
}

Minimal API会默认添加针对路由的服务注册,完成路由的两个中间件(RoutingMiddleware和EndpointRoutingMiddleware)也会在自动注册到创建的WebApplication对象上。WebApplication类型同时实现了IEndpointRouteBuilder接口,我们只需要利用它注册相应的终结点就可以了。如下的演示程序调用了WebApplication对象的MapGet方法注册了一个仅针对GET请求的终结点,终结点采用的路径模板为“weather/{city}/{days}”,携带的两个路由参数({city}和{days})分别代表目标城市代码(区号)和天数。

using App;
var app = WebApplication.Create();
app.MapGet("weather/{city}/{days}", ForecastAsync);
app.Run(); static Task ForecastAsync(HttpContext context)
{
var routeValues = context.GetRouteData().Values;
var city = routeValues["city"]!.ToString();
var days = int.Parse(routeValues["days"]!.ToString()!);
var report = WeatherReportUtility.Generate(city!, days);
return WeatherReportUtility.RenderAsync(context, report);
}

注册中间件采用的处理器是一个RequestDelegate委托,我们将它指向ForecastAsync方法。该方法调用HttpContext上下文的GetRouteData方法得到承载“路由数据”的RouteData对象,后者的Values属性返回路由参数字典。我们从中提取出代表城市代码和天数的路由参数,并创建出对应的天气报告,最后将其转换成HTML作为响应内容。

[S2002]以内联方式设置路由参数的约束

上面的演示实例注册的路由模板中定义了两个参数({city}和{days}),分别表示获取天气预报的目标城市对应的区号和天数。区号应该具有一定的格式(以零开始的3~4位数字),而天数除了必须是一个整数,还应该具有一定的范围。由于没有对这两个路由参数坐任何约束,所以请求URL携带的任何字符都是有效的。ForecastAsync方法也并没有对提取的路由参数做任何验证,所以在执行过程中面对不合法的输入会直接抛出异常。

为了确保路由参数值的有效性,在进行中间件注册时可以采用内联(Inline)的方式直接将相应的约束规则定义在路由模板中。ASP.NET为常用的验证规则定义了相应的约束表达式,我们可以根据需要为某个路由参数指定一个或者多个约束表达式。如下面的代码片段所示,我们为路由参数“{city}”指定了一个基于“区号”的正则表达式(“:regex(^0[1-9]{{2,3}}$)”)。另一个路由参数{days}则应用了两个约束,一个是针对数据类型的约束(“:int”),另一个是针对区间的约束(“:range(1,4)”)。

using App;
var template = @"weather/{city:regex(^0\d{{2,3}}$)}/{days:int:range(1,4)}";
var app = WebApplication.Create();
app.MapGet(template, ForecastAsync);
app.Run();

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

图2 不满足路由约束而返回的“404 Not Found”响应

[S2003]定义可缺省的路由参数

路由模板(如“weather/{city}/{days}”)可以包含静态的字符(如“weather”),也可以包含动态的参数(如{city}和{days}),我们将后者称为路由参数。并非每个路由参数都必须有请求URL对应的部分来指定,如果赋予路由参数一个默认值,那么它在请求URL中就是可以缺省的。对上面演示的实例来说,我们可以采用如下方式在路由参数名后面添加一个问号(“?”)将原本必需的路由参数变成可以缺省的默认参数的。可以缺省的路由参数与在方法中定义可缺省的(Optional)params参数一样,只能出现在路由模板尾部。

using App;

var template = "weather/{city?}/{days?}";
var app = WebApplication.Create();
app.MapGet(template, ForecastAsync);
app.Run(); static Task ForecastAsync(HttpContext context)
{
var routeValues = context.GetRouteData().Values;
var city = routeValues.TryGetValue("city", out var v1) ? v1!.ToString() : "010";
var days = routeValues.TryGetValue("days", out var v2) ? v1!.ToString() : "4";
var report = WeatherReportUtility.Generate(city!, int.Parse(days!));
return WeatherReportUtility.RenderAsync(context, report);
}

既然路由变量占据的部分路径是可以缺省的,那么即使请求的URL不具有对应的值(如“weather”和“weather/010”),它与路由规则也是匹配的,但此时在路由参数字典中是找不到它们的。此时我们不得不对处理请求的ForecastAsync方法进行相应的改动。针对上述改动,如果希望获取北京未来4天的天气状况,我们可以采用图20-3所示的三种URL(“weather”、“weather/010”和“weather/010/4”),这三个请求的URL本质上是完全等效的。

图3 不同URL针对默认路由参数的等效性

[S2004]为路由参数指定默认值

实际上可缺省路由参数默认值的设置还有一种更简单的方式,那就是按照如下所示的方式直接将默认值定义在路由模板中。这样针对ForecastAsync方法的改动就完全没有必要。

using App;

var template = @"weather/{city=010}/{days=4}";
var app = WebApplication.Create();
app.MapGet(template, ForecastAsync);
app.Run(); static Task ForecastAsync(HttpContext context)
{
var routeValues = context.GetRouteData().Values;
var city = routeValues["city"]!.ToString();
var days = int.Parse(routeValues["days"]!.ToString()!);
var report = WeatherReportUtility.Generate(city!, days);
return WeatherReportUtility.RenderAsync(context, report);
}

[S2005]一个路径分段定义多个路由参数

一个URL可以通过分隔符“/”划分为多个路径分段(Segment),路由参数一般来说会占据某个独立的分段(如“weather/{city}/{days}”)。但也有例外情况,我们既可以在一个单独的路径分段中定义多个路由参数,也可以让一个路由参数跨越多个连续的路径分段。以我们的演示程序为例,我们需要设计一种路径模式来获取某个城市某一天的天气信息,如使用“/weather/010/2019.11.11”这样URL获取北京在2019年11月11日的天气,对应模板为“/weather/{city}/{year}.{month}.{day}”。

using App;

var template = "weather/{city}/{year}.{month}.{day}";
var app = WebApplication.Create();
app.MapGet(template, ForecastAsync);
app.Run(); static Task ForecastAsync(HttpContext context)
{
var routeValues = context.GetRouteData().Values;
var city = routeValues["city"]!.ToString();
var year = int.Parse(routeValues["year"]!.ToString()!);
var month = int.Parse(routeValues["month"]!.ToString()!);
var day = int.Parse(routeValues["day"]!.ToString()!);
var report = WeatherReportUtility.Generate(city!, new DateTime(year,month,day));
return WeatherReportUtility.RenderAsync(context, report);
}

对于修改后的程序,如果采用“/weather/{city}/{yyyy}.{mm}.{dd}”这样的URL,我们就可以获取某个城市指定日期的天气。如图4所示,我们采用请求路径“/weather/010/2019.11.11”可以获取北京在2019年11月11日的天气。

图4 一个路径分段定义多个路由参数

[S2006]一个路由参数跨越多个路径分段

上面设计的路由模板采用“.”作为日期分隔符,如果采用“/”作为日期分隔符(如2019/11/11),这个路由默认应该如何定义呢?由于“/”同时也是路径分隔符,就意味着同一个路由参数跨越了多个路径分段,这种情况只能采用“通配符”的形式才能达成我们的目标。通配符路由参数采用{*variable}或者{**variable}的形式,星号(*)表示路径“余下的部分”,所以这样的路由参数也只能出现在模板的尾端。演示程序的路由模板可以定义成“/weather/{city}/{*date}”。

using App;
using System.Globalization; var template = "weather/{city}/{*date}";
var app = WebApplication.Create();
app.MapGet(template, ForecastAsync);
app.Run(); static Task ForecastAsync(HttpContext context)
{
var routeValues = context.GetRouteData().Values;
var city = routeValues["city"]!.ToString();
var date = DateTime.ParseExact(routeValues["date"]?.ToString()!,"yyyy/MM/dd",CultureInfo.InvariantCulture);
var report = WeatherReportUtility.Generate(city!, date);
return WeatherReportUtility.RenderAsync(context, report);
}

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

图5 一个路由参数跨越多个路径分段

[S2007]主机名绑定

一般来说,在利用某路由终结点与待路由的请求进行匹配的时候只需要考虑请求地址的路径部分,并忽略主机(Host)名称和端口号,但是一定要加上针对主机名称(含端口)的匹配策略也未尝不可。在如下这个演示程序中,我们通过调用MapGet扩展方法为根路径“/”添加了三个路由终结点,并调用该方法返回的IEndpointConventionBuilder对象的RequireHost扩展方法绑定了对应的主机名(“*.artech.com”、“www.foo.artech.com”和“www.foo.artech.com:9999”)。指定的第一个主机名包含一个前置通配符“*”,最后一个则指定了端口号。注册的这三个终结点会直接将指定的主机名作为响应内容。

var app = WebApplication.Create();
app.Urls.Add("http://0.0.0.0:6666");
app.Urls.Add("http://0.0.0.0:9999");
app
.MapHost("*.artech.com")
.MapHost("www.foo.artech.com")
.MapHost("www.foo.artech.com:9999");
app.Run(); internal static class Extensions
{
public static IEndpointRouteBuilder MapHost(this IEndpointRouteBuilder endpoints,string host)
{
endpoints.MapGet("/", context => context.Response.WriteAsync(host)).RequireHost(host);
return endpoints;
}
}

为了能够在本机采用不同的域名对演示应用发起请求,我们通过修改Hosts文件的方式将本地地址(“127.0.0.1”)映射为多个不同的域名。我们以管理员(Administrator)身份打开文件Hosts “%windir%\System32\drivers\etc\hosts”,并以如下所示的方式添加了针对两个域名的映射。

127.0.0.1 www.foo.artech.com
127.0.0.1 www.bar.artech.com

应用启动之后,我们利用浏览器使用不同的域名和端口对其发起请求,并得到如图6所示的输出结果。输出的内容不仅仅体现了终结点选择过程中针对主机名的过滤,还体现了终结点选择策略的一个重要的特性,那就是路由系统总是试图选择一个与当前请求匹配度最高的终结点,而不是选择第一个匹配的终结点。

图6 主机名绑定

[S2008]将终结点处理定义为任意类型的委托

上面的例子都直接使用一个RequestDelegate委托作为终结点的处理器,实际上我们在注册终结点时可以将处理器设置为任何类型的委托都可以。当路由请求分发给注册的委托进行处理器时,会尽可能地从当前HttpContext上下文中提取相应的数据对委托的输入参数进行绑定。对于委托的执行结果,路由系统也会按照预定义的规则“智能”地将它应用到针对请求的响应中。按照这个规则,我们演示程序中用来处理请求的ForecastAsync方法可以简写成如下形式。第一个参数会自动绑定为当前HttpContext上下文,后面的两个参数则自动与同名的路由参数进行绑定。

using App;

var app = WebApplication.Create();
app.MapGet("weather/{city}/{days}", ForecastAsync);
app.Run(); static Task ForecastAsync(HttpContext context, string city, int days)
{
var report = WeatherReportUtility.Generate(city,days);
return WeatherReportUtility.RenderAsync(context, report);
}

[S2009]IResult 的应用

不论终结点处理器的委托返回何种类型的对象,路由系统总能做出对应的处理。比如对于返回的字符串会直接作为响应的主体内容,并将Content-Type报头设置为“text/plain”。如果希望对返回对象具有明确的控制,最好返回一个IResult对象(或者Task<IResult>和ValueTask<IResult>),IResult相当ASP.NET MVC中的IActionResult。我们演示程序中的ForecastAsync方法也可以改写成如下这个返回类型为IResult的Forecast方法,该方法通过调用Results类型的静态Content方法返回一个ContentResult对象,它将天气报告转换成的HTML作为响应类型,Content-Type报头设置为 “text/html” 。

using App;

var app = WebApplication.Create();
app.MapGet("weather/{city}/{days}", Forecast);
app.Run(); static IResult Forecast(HttpContext context, string city, int days)
{
var report = WeatherReportUtility.Generate(city,days);
return Results.Content(WeatherReportUtility.AsHtml(report), "text/html");
}

ASP.NET Core 6框架揭秘实例演示[30]:利用路由开发REST API的更多相关文章

  1. ASP.NET Core 6框架揭秘实例演示[31]:路由&ldquo;高阶&rdquo;用法

    ASP.NET的路由是通过EndpointRoutingMiddleware和EndpointMiddleware这两个中间件协作完成的,它们在ASP.NET平台上具有举足轻重的地位,MVC和gRPC ...

  2. ASP.NET Core 6框架揭秘实例演示[07]:文件系统

    ASP.NET Core应用具有很多读取文件的场景,如读取配置文件.静态Web资源文件(如CSS.JavaScript和图片文件等).MVC应用的视图文件,以及直接编译到程序集中的内嵌资源文件.这些文 ...

  3. ASP.NET Core 6框架揭秘实例演示[08]:配置的基本编程模式

    .NET的配置支持多样化的数据源,我们可以采用内存的变量.环境变量.命令行参数.以及各种格式的配置文件作为配置的数据来源.在对配置系统进行系统介绍之前,我们通过几个简单的实例演示一下如何将具有不同来源 ...

  4. ASP.NET Core 6框架揭秘实例演示[09]:配置绑定

    我们倾向于将IConfiguration对象转换成一个具体的对象,以面向对象的方式来使用配置,我们将这个转换过程称为配置绑定.除了将配置树叶子节点配置节的绑定为某种标量对象外,我们还可以直接将一个配置 ...

  5. ASP.NET Core 6框架揭秘实例演示[10]:Options基本编程模式

    依赖注入使我们可以将依赖的功能定义成服务,最终以一种松耦合的形式注入消费该功能的组件或者服务中.除了可以采用依赖注入的形式消费承载某种功能的服务,还可以采用相同的方式消费承载配置数据的Options对 ...

  6. ASP.NET Core 6框架揭秘实例演示[11]:诊断跟踪的几种基本编程方式

    在整个软件开发维护生命周期内,最难的不是如何将软件系统开发出来,而是在系统上线之后及时解决遇到的问题.一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根 ...

  7. ASP.NET Core 6框架揭秘实例演示[12]:诊断跟踪的进阶用法

    一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根据当前的运行状态预知未来可能发生的问题,并将问题扼杀在摇篮中.诊断跟踪能够帮助我们有效地纠错和排错&l ...

  8. ASP.NET Core 6框架揭秘实例演示[13]:日志的基本编程模式[上篇]

    <诊断跟踪的几种基本编程方式>介绍了四种常用的诊断日志框架.其实除了微软提供的这些日志框架,还有很多第三方日志框架可供我们选择,比如Log4Net.NLog和Serilog 等.虽然这些框 ...

  9. ASP.NET Core 6框架揭秘实例演示[14]:日志的进阶用法

    为了对各种日志框架进行整合,微软创建了一个用来提供统一的日志编程模式的日志框架.<日志的基本编程模式>以实例演示的方式介绍了日志的基本编程模式,现在我们来补充几种"进阶" ...

随机推荐

  1. ASP.NET Core + SaasKit + PostgreSQL + Citus 的多租户应用程序架构示例

    在 确定分布策略 中, 我们讨论了在多租户用例中使用 Citus 所需的与框架无关的数据库更改. 当前部分研究如何构建与 Citus 存储后端一起使用的多租户 ASP.NET 应用程序. http:/ ...

  2. kernel heap bypass smep,smap && 劫持modprobe_path

    kernel heap bypass smep,smap && 劫持modprobe_path exp1 smep:smep即用户数据不可执行,当 CPU 处于 ring0 模式时,执 ...

  3. 五、C++运算符重载,使面向对象编程更方便

    复数类CComplex 编译器做对象运算的时候,会调用对象的运算符重载函数(优先调用成员方法):如果没有成员方法,就砸全局作用域找合适的运算符重载函数 ++和--运算符是单目运算符,在参数列表里放上一 ...

  4. 前端js堆栈

    1.介绍创建数据的时候就会占用内容.内存主要开辟了两类空间1. 堆(进程,线程共享) 大小不固定,可随时增加不允许js直接访问堆内存存储引用类型数据按引用访问存储的值大小不定,可动态调整主要用来存放对 ...

  5. 136_Power BI 自定义矩阵热力图

    博客:www.jiaopengzi.com 焦棚子的文章目录 请点击下载附件 一.背景 在前几天在pbirs自定义登录后,发现一个问题就是非Power BI原生的视觉无法显示,改回AD登录又可以了.目 ...

  6. 安装Nmap到CentOS(YUM)

    Nmap是Linux下的网络扫描工具,我们可以扫描远端主机上那些端口在开放状态. 运行环境 系统版本:CentOS Linux release 7.3.1611 (Core) 软件版本:无 硬件要求: ...

  7. 分享JAVA的FTP和SFTP相关操作工具类

     1.导入相关jar <!--FTPClient--><dependency> <groupId>commons-net</groupId> <a ...

  8. Redis基础与性能调优

    Redis是一个开源的,基于内存的结构化数据存储媒介,可以作为数据库.缓存服务或消息服务使用. Redis支持多种数据结构,包括字符串.哈希表.链表.集合.有序集合.位图.Hyperloglogs等. ...

  9. UiPathExcel读取操作

    一.Uipath操作Excel的相关基本概念 1.UiPath操作Excel的两组方法 App Integration > Excel   VS  System > File > W ...

  10. 抓包整理外篇fiddler————了解工具栏[一]

    前言 抓包本篇还没写完,因为在工作中,发现有人用fiddler 用的还不是很好,所以去介绍一下这个东西,fiddler大体分为10多个章节. 正文 首先了解一下fiddler的抓包原理哈. 可以看到当 ...