【.NET Core框架】终结点路由(Routing)
介绍
早期路由系统
.NET Core2.2之前的框架中,中间件管道的结尾有一个Router中间件,也就是路由中间件,这个路由中间件会把HTTP请求和路由数据发送给MVC的一个组件,它叫做MVC Router Handler。
这个MVC 路由 Handler就会使用这些路由数据来决定哪个Controller的Action方法应该来负责处理这个请求。
早期路由系统的弊端:
在Router中间件之前的中间件不知道请求被哪个Controller/Action处理了。如果你在中间件中想知道被哪个Action处理了,必须将逻辑写在Router中间件中,导致职责耦合,或者是写在过滤器中。
在core中授权中间件、CORS中间件都支持策略,通过Contrller上的Attribute获取对应策略,所以需要在Router中间件之前知道请求被路由到哪个Controller/Action,以及标识的Attribute等信息
Endpoint Routing(终结点路由系统)
Endpoint routing 路由系统解决了以上问题。终结点路由是通过EndpointRoutingMiddleware和EndpointMiddleware这两个中间件协作完成的,这两个中间件类型都定义在NuGet包“Microsoft.AspNetCore.Routing”中。
- app.UseRouting():用于向中间件管道添加路由匹配中间件(EndpointRoutingMiddleware)。该中间件会检查应用中定义的终结点列表,然后通过匹配 URL 和 HTTP 方法来选择最佳的终结点。简单说,该中间件的作用是根据一定规则来选择出终结点
- app.UseEndpoints()。用于向中间件管道添加终结点中间件(EndpointMiddleware)。可以向该中间件的终结点列表中添加终结点,并配置这些终结点要执行的委托,该中间件会负责运行由EndpointRoutingMiddleware中间件选择的终结点所关联的委托。简单说,该中间件用来执行所选择的终结点委托,从而让其生成响应的
这样做的好处就是,我们可以在选择端点和执行端点的中间位置插入其它的中间件。这样的话,插入到中间位置的中间件就会知道哪个端点被选取了,而且它也有可能会选择其它的端点
程序启动的时候会把所有的Controller 中的Action 映射存储到routeOptions 的集合中,Action 映射成Endpoint终结者 的RequestDelegate 委托属性,最后通过UseEndPoints 添加EndpointMiddleware 中间件进行执行,同时这个中间件中的Endpoint 终结点路由已经是通过Routing匹配后的路由。
Endpoint
Endpoint类中包含一个请求的委托(Request Delegate)和元数据。终结点通过RequestDelegate对象来处理路由过来的请求。
而在MVC的上下文中,这个请求委托就是一个包装类,它包装了一个方法,这个方法可以实例化一个Controller并执行选中的Action方法。
Endpoint还包含元数据,这些元数据用来决定他们的请求委托是否应该用于当前的请求,还是另有其它用途,路由过程中的很多行为都可以通过相应的元数据来控制。
终结点有以下特点:
- 可执行:含有RequestDelegate委托
- 可扩展:含有Metadata元数据集合
- 可选择:可选的包含路由信息
- 可枚举:通过DI容器,查找EndpointDataSource来展示终结点集合。
第一个HTTP请求进来的时候,Endpoint Routing中间件就会把请求映射到一个Endpoint上。它会使用之App启动时创建好的EndpointDataSource,来遍历查找所有可用的Endpoint,并检查和它关联的路由以及元数据,来找到最匹配的Endpoint。
一旦某个Endpoint实例被选中,它就会被附加在请求的对象上,这样它就可以被后续的中间件所使用了。
最后在管道的尽头,当 Endpoint中间件运行的时候,它就会执行Endpoint所关联的请求委托。这个请求委托就会触发和实例化选中的Controller和Action方法,并产生响应。最后响应再从中间件管道原路返回。
EndpointRoutingMiddleware(Endpoint Routing中间件)
Endpoint Routing 中间件会分析进来的请求,并把它和在程序中注册的Endpoints进行比较。它会使用这些 Endpoints 上面的元数据来决定哪个是处理该请求的最佳人选。然后,这个选中的Endpoint 就会被赋给请求的对象,而其它后续的中间件就可以根据这个选中的Endpoint,来做一些自己的决策。在所有的中间件都执行完之后,这个被选中的Endpoint最终将被 Endpoint中间件所执行,而与之关联的Action方法就会被执行。
如果中间件中需要获取到Controller、Action信息,将中间件放到Endpoint Routing和Endpoint两个中间件之间,通过以下代码获取信息
Endpoint endpoint = httpContext.GetEndpoint();
ControllerActionDescriptor controllerActionDescriptor = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>();
var controllerType = controllerActionDescriptor.ControllerTypeInfo;//Controller Type
var allowAnonymous = endpoint.Metadata.GetMetadata<IAllowAnonymous>();//AllowAnonymousAttribute
在中间件管道中获取路由选择的终结点
在中间件管道中,我们可以通过HttpContext来检索终结点等信息。需要注意的是,终结点对象在创建完毕后,是不可变的,无法修改。
- 在调用UseRouting之前,你可以注册一些用于修改路由操作的数据,比如UseRewriter、UseHttpMethodOverride、UsePathBase等。
- 在调用UseRouting和UseEndpoints之间,可以注册一些用于提前处理路由结果的中间件,如UseAuthentication、UseAuthorization、UseCors等。
我们一起看下面的代码:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.Use(next => context =>
{
// 在 UseRouting 调用前,始终为 null
Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "null"}");
return next(context);
});
// EndpointRoutingMiddleware 调用 SetEndpoint 来设置终结点
app.UseRouting();
app.Use(next => context =>
{
// 如果路由匹配到了终结点,那么此处就不为 null,否则,还是 null
Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "null"}");
return next(context);
});
// EndpointMiddleware 通过 GetEndpoint 方法获取终结点,
// 然后执行该终结点的 RequestDelegate 委托
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", context =>
{
// 匹配到了终结点,肯定不是 null
Console.WriteLine($"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "null"}");
return Task.CompletedTask;
}).WithDisplayName("Custom Display Name"); // 自定义终结点名称
});
app.Use(next => context =>
{
// 只有当路由没有匹配到终结点时,才会执行这里
Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "null"}");
return next(context);
});
}
当访问/时,输出为:
1. Endpoint: null
2. Endpoint: Custom Display Name
3. Endpoint: Custom Display Name
当访问其他不匹配的URL时,输出为:
1. Endpoint: null
2. Endpoint: null
4. Endpoint: null
当路由匹配到了终结点时,EndpointMiddleware则是该路由的终端中间件;当未匹配到终结点时,会继续执行后面的中间件。
配置终结点委托
可以通过以下方法将委托关联到终结点
- MapGet
- MapPost
- MapPut
- MapDelete
- MapHealthChecks
其他类似“MapXXX”的方法
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context => await context.Response.WriteAsync("get"));
endpoints.MapPost("/", async context => await context.Response.WriteAsync("post"));
endpoints.MapPut("/", async context => await context.Response.WriteAsync("put"));
endpoints.MapDelete("/", async context => await context.Response.WriteAsync("delete"));
endpoints.MapHealthChecks("/healthChecks");
endpoints.MapControllers();
});
}
路由模板
规则:
- 通过{}来绑定路由参数,如:
- 将?作为参数后缀,以指示该参数是可选的,如:
- 通过=设置默认值,如:{name=jjj} 表示name的默认值是jjj
- 通过:添加内联约束,如:{id:int},后面追加:可以添加多个内联约束,如:
- 多个路由参数间必须通过文本或分隔符分隔,例如 {a}{b} 就不符合规则,可以修改为类似 {a}+-{b} 或 {a}/{b} 的形式
- 先举个例子,/book/{name}中的{name}为路由参数,book为非路由参数文本。
- 如果要匹配{或},则使用{{或}}进行转义
catch-all参数
一个URL可以通过分隔符“/”划分为多个路径分段(Segment),路由模板中定义的路由参数一般来说会占据某个独立的分段(如“weather/{city}/{days}”)。但也有例外情况,我们既可以在一个单独的路径分段中定义多个路由参数,也可以让一个路由参数跨越多个连续的路径分段。
一个分段包含多个路由参数:/weather/{city}/{year}.{month}.{day}
一个参数跨越多个分段,我们只能采用定义“通配符”的形式来达到这个目的,通配符路由参数采用{*variable}或者{**variable}的形式,星号表示路径“余下的部分”,所以这样的路由参数只能出现在模板的尾端:/weather/{city}/{*date}
*和**在一般使用上没有什么区别,它们仅仅在使用LinkGenerator时会有不同,如id = abc/def,当使用/Book/{*id}模板时,会生成/Book/abc%2Fdef,当使用/Book/{**id}模板时,会生成/Book/abc/def。
路由约束
约束类型
int:如{age:int}
bool
datetime
decimal
double
float
guid
long
minlength:要求参数值表示的字符串不小于指定的长度{age:minlength(2)}
maxlength:要求参数值表示的字符串不大于指定的长度
length:要求参数值表示的字符串长度限于指定的区间范围{age:length(2,3)}
min:最小值
max:最大值
range:要求参数值介于指定的区间范围
alpha:所有字符必须是字母
required:要求参数值不能是空字符串
file:要求参数值可以作为一个包含扩展名的文件名
nonfile:要求参数值不是文件名,与上相反
regex:正则表达式约束::regex(正则表达式)
正则表达式路由约束
通过regex(expression)来设置正则表达式约束
另外,还需要注意对某些字符进行转义:
- \替换为\\
- {替换为{{, }替换为}}
- [替换为[[,]替换为]]
指定 regex 约束的两种方式:
// 内联方式
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
context =>
{
return context.Response.WriteAsync("inline-constraint match");
});
});
// 变量声明方式
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "people",
pattern: "People/{ssn}",
constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
defaults: new { controller = "People", action = "List", });
});
路由模板优先级
考虑一下,有两个路由模板:/Book/List和/Book/{id},当url为/Book/List时,会选择哪个呢?从结果我们可以得知,是模板/Book/List。它是根据以下规则来确定的:
- 越具体的模板优先级越高
- 包含更多匹配段的模板更具体
- 含有文本的段比参数段更具体
- 具有约束的参数段比没有约束的参数段更具体
- 复杂段和具有约束的段同样具体
- catch-all参数段是最不具体的
属性路由
webapi更适合属性路由,添加以下代码可以支持属性路由
EndpointMiddleware(Endpoint中间件)
EndpointMiddleware的逻辑很简单,仅仅是执行选中的Endpoint中的RequestDelegate方法
源码:(删掉了日志相关代码)
internal sealed class EndpointMiddleware
{
internal const string AuthorizationMiddlewareInvokedKey = "__AuthorizationMiddlewareWithEndpointInvoked";
internal const string CorsMiddlewareInvokedKey = "__CorsMiddlewareWithEndpointInvoked";
private readonly ILogger _logger;
private readonly RequestDelegate _next;
private readonly RouteOptions _routeOptions;
public EndpointMiddleware(
ILogger<EndpointMiddleware> logger,
RequestDelegate next,
IOptions<RouteOptions> routeOptions)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_next = next ?? throw new ArgumentNullException(nameof(next));
_routeOptions = routeOptions?.Value ?? throw new ArgumentNullException(nameof(routeOptions));
}
public Task Invoke(HttpContext httpContext)
{
var endpoint = httpContext.GetEndpoint();
if (endpoint?.RequestDelegate != null)
{
if (!_routeOptions.SuppressCheckForUnhandledSecurityMetadata)
{
if (endpoint.Metadata.GetMetadata<IAuthorizeData>() != null &&
!httpContext.Items.ContainsKey(AuthorizationMiddlewareInvokedKey))
{
ThrowMissingAuthMiddlewareException(endpoint);
}
if (endpoint.Metadata.GetMetadata<ICorsMetadata>() != null &&
!httpContext.Items.ContainsKey(CorsMiddlewareInvokedKey))
{
ThrowMissingCorsMiddlewareException(endpoint);
}
}
try
{
var requestTask = endpoint.RequestDelegate(httpContext);
if (!requestTask.IsCompletedSuccessfully)
{
return AwaitRequestTask(endpoint, requestTask, _logger);
}
}
catch (Exception exception)
{
return Task.FromException(exception);
}
return Task.CompletedTask;
}
return _next(httpContext);
static async Task AwaitRequestTask(Endpoint endpoint, Task requestTask, ILogger logger)
{
await requestTask;
}
}
}
ControllerActionEndpointDataSource
ControllerActionEndpointDataSource里面存储着在应用程序里注册的路由模板
ControllerActionEndpointDataSource类作为应用程序级别的服务被创建了
这个类里面有一个叫做CreateEndpoints()的方法,它会获取所有Controller的Action方法
然后针对每个Action方法,它会创建一个Endpoint实例。这些Endpoint实例就是包装了Controller和Action方法的执行的请求委托(Request Delegate)
而针对每个Endpoint,它要么与某个按约定的路由模板相关联,要么与某个Controller Action上的Attribute路由信息相关联。而这些路由在稍后就会被用来将Endpoint与进来的请求进行匹配。
参考:
https://www.cnblogs.com/artech/p/endpoint-middleware-01.html
https://www.cnblogs.com/cgzl/p/12561571.html
https://www.cnblogs.com/xiaoxiaotank/p/15468491.html