注:本文隸屬於《理解ASP.NET Core》系列文章,請查看置頂博客或點擊此處查看全文目錄
Routing
- Routing(路由):更准確的應該叫做Endpoint Routing,負責將HTTP請求按照匹配規則選擇對應的終結點
- Endpoint(終結點):負責當HTTP請求到達時,執行代碼
路由是通過UseRouting
和UseEndpoints
兩個中間件配合在一起來完成注冊的:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// 添加Routing相關服務
// 注意,其已在 ConfigureWebDefaults 中添加,無需手動添加,此處僅為演示
services.AddRouting();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
}
}
UseRouting
:用於向中間件管道添加路由匹配中間件(EndpointRoutingMiddleware
)。該中間件會檢查應用中定義的終結點列表,然后通過匹配 URL 和 HTTP 方法來選擇最佳的終結點。簡單說,該中間件的作用是根據一定規則來選擇出終結點UseEndpoints
:用於向中間件管道添加終結點中間件(EndpointMiddleware
)。可以向該中間件的終結點列表中添加終結點,並配置這些終結點要執行的委托,該中間件會負責運行由EndpointRoutingMiddleware
中間件選擇的終結點所關聯的委托。簡單說,該中間件用來執行所選擇的終結點委托
UseRouting
與UseEndpoints
必須同時使用,而且必須先調用UseRouting
,再調用UseEndpoints
Endpoints
先了解一下終結點的類結構:
public class Endpoint
{
public Endpoint(RequestDelegate requestDelegate, EndpointMetadataCollection? metadata, string? displayName);
public string? DisplayName { get; }
public EndpointMetadataCollection Metadata { get; }
public RequestDelegate RequestDelegate { get; }
public override string? ToString();
}
終結點有以下特點:
- 可執行:含有
RequestDelegate
委托 - 可擴展:含有
Metadata
元數據集合 - 可選擇:可選的包含路由信息
- 可枚舉:通過DI容器,查找
EndpointDataSource
來展示終結點集合。
在中間件管道中獲取路由選擇的終結點
對於中間件還不熟悉的,可以先看一下中間件(Middleware)。
在中間件管道中,我們可以通過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.UseAuthorization();
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
為非路由參數文本。非路由參數的文本和分隔符/
:- 是不分區大小寫的(官方中文文檔翻譯錯了)
- 要使用沒有被Url編碼的格式,如空格會被編碼為 %20,不應使用 %20,而應使用空格
- 如果要匹配
{
或}
,則使用{{
或}}
進行轉義
catch-all參數
路由模板中的星號*
和雙星號**
被稱為catch-all參數,該參數可以作為路由參數的前綴,如/Book/{*id}
、/Book/{**id}
,可以匹配以/Book
開頭的任意Url,如/Book
、/Book/
、/Book/abc
、/Book/abc/def
等。
*
和**
在一般使用上沒有什么區別,它們僅僅在使用LinkGenerator
時會有不同,如id = abc/def
,當使用/Book/{*id}
模板時,會生成/Book/abc%2Fdef
,當使用/Book/{**id}
模板時,會生成/Book/abc/def
。
復雜段
復雜段通過非貪婪的方式從右到左進行匹配,例如[Route("/a{b}c{d}")]
就是一個復雜段。實際上,它的確很復雜,只有了解它的工作方式,才能正確的使用它。
- 貪婪匹配(也稱為“懶惰匹配”):匹配最大可能的字符串
- 非貪婪匹配:匹配最小可能的字符串
接下來,就拿模板[Route("/a{b}c{d}")]
來舉兩個例子:
成功匹配的案例——當Url為/abcd
時,匹配過程為(|
用於輔助展示算法的解析方式):
- 從右到左讀取模板,找到的第一個文本為
c
。接着,讀取Url/abcd
,可解析為/ab|c|d
- 此時,Url中右側的所有內容
d
均與路由參數{d}
匹配 - 然后,繼續從右到左讀取模板,找到的下一個文本為
a
。接着,從剛才停下的地方繼續讀取Url/ab|c|d
,解析為/a|b|c|d
- 此時,Url中右側的值
b
與路由參數{b}
匹配 - 最后,沒有剩余的路由模板段或參數,也沒有剩余的Url文本,因此匹配成功。
匹配失敗的案例——當Url為/aabcd
時,匹配過程為(|
用於輔助展示算法的解析方式):
- 從右到左讀取模板,找到的第一個文本為
c
。接着,讀取Url/aabcd
,可解析為/aab|c|d
- 此時,Url中右側的所有內容
d
均與路由參數{d}
匹配 - 然后,繼續從右到左讀取模板,找到的下一個文本為
a
。接着,從剛才停下的地方繼續讀取Url/aab|c|d
,解析為/a|a|b|c|d
- 此時,Url中右側的值
b
與路由參數{b}
匹配 - 最后,沒有剩余的路由模板段或參數,但還有剩余的Url文本,因此匹配不成功。
使用復雜段,相比普通路由模板來說,會造成更加昂貴的性能影響
路由約束
通過路由約束,可以在路由匹配過程中,檢查URL是否是可接受的。另外,路由約束一般是用來消除路由歧義,而不是用來進行輸入驗證的。
實現上,當Http請求到達時,路由參數和該參數的約束名會傳遞給IInlineConstraintResolver
服務,IInlineConstraintResolver
服務會負責創建IRouteConstraint
實例,以針對Url進行處理。
預定義的路由約束
摘自官方文檔
約束 | 示例 | 匹配項示例 | 說明 |
---|---|---|---|
int | {id:int} |
123456789, -123456789 | 匹配任何整數 |
bool | {active:bool} |
true, FALSE | 匹配 true 或 false。 不區分大小寫 |
datetime | {dob:datetime} |
2016-12-31, 2016-12-31 7:32pm | 匹配固定區域中的有效 DateTime 值 |
decimal | {price:decimal} |
49.99, -1,000.01 | 匹配固定區域中的有效 decimal 值。 |
double | {weight:double} |
1.234, -1,001.01e8 | 匹配固定區域中的有效 double 值。 |
float | {weight:float} |
1.234, -1,001.01e8 | 匹配固定區域中的有效 float 值。 |
guid | {id:guid} |
CD2C1638-1638-72D5-1638-DEADBEEF1638 | 匹配有效的 Guid 值 |
long | {ticks:long} |
123456789, -123456789 | 匹配有效的 long 值 |
minlength(value) | {username:minlength(4)} |
Rick | 字符串必須至少為 4 個字符 |
maxlength(value) | {filename:maxlength(8)} |
MyFile | 字符串不得超過 8 個字符 |
length(length) | {filename:length(12)} |
somefile.txt | 字符串必須正好為 12 個字符 |
length(min,max) | {filename:length(8,16)} |
somefile.txt | 字符串必須至少為 8 個字符,且不得超過 16 個字符 |
min(value) | {age:min(18)} |
19 | 整數值必須至少為 18 |
max(value) | {age:max(120)} |
91 | 整數值不得超過 120 |
range(min,max) | {age:range(18,120)} |
91 | 整數值必須至少為 18,且不得超過 120 |
alpha | {name:alpha} |
Rick | 字符串必須由一個或多個字母字符組成,a-z,並區分大小寫。 |
regex(expression) | {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} |
123-45-6789 | 字符串必須與正則表達式匹配 |
required | {name:required} |
Rick | 用於強制在 URL 生成過程中存在非參數值 |
正則表達式路由約束
通過regex(expression)
來設置正則表達式約束,並且該正則表達式是:
RegexOptions.IgnoreCase
:忽略大小寫RegexOptions.Compiled
:將該正則表達式編譯為程序集。這會使得執行速度更快,但會拖慢啟動時間。RegexOptions.CultureInvariant
:忽略區域文化差異。
另外,還需要注意對某些字符進行轉義:
\
替換為\\
{
替換為{{
,}
替換為}}
[
替換為[[
,]
替換為]]
例如:
標准正則表達式 | 轉義的正則表達式 |
---|---|
^\d{3}-\d{2}-\d{4}$ |
^\\d{{3}}-\\d{{2}}-\\d{{4}}$ |
^[a-z]{2}$ |
^[[a-z]]{{2}}$ |
- 指定 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", });
});
不要書寫過於復雜的正則表達式,否則,相比普通路由模板來說,會造成更加昂貴的性能影響
自定義路由約束
先說一句,自定義路由約束很少會用到,在你決定要自定義路由約束之前,先想想是否有其他更好的替代方案,如使用模型綁定。
通過實現IRouteConstraint
接口來創建自定義路由約束,該接口僅有一個Match
方法,用於驗證路由參數是否滿足約束,返回true
表示滿足約束,false
則表示不滿足約束。
以下示例要求路由參數中必須包含字符串“1”:
public class MyRouteConstraint : IRouteConstraint
{
public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
{
if (values.TryGetValue(routeKey, out object value))
{
var valueStr = Convert.ToString(value, CultureInfo.InvariantCulture);
return valueStr?.Contains("1") ?? false;
}
return false;
}
}
然后進行路由約束注冊:
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting(options =>
{
// 添加自定義路由約束,約束 Key 為 my
options.ConstraintMap["my"] = typeof(MyRouteConstraint);
});
}
最后你就可以類似如下進行使用了:
[HttpGet("{id:my}")]
public string Get(string id)
{
return id;
}
路由模板優先級
考慮一下,有兩個路由模板:/Book/List
和/Book/{id}
,當url為/Book/List
時,會選擇哪個呢?從結果我們可以得知,是模板/Book/List
。它是根據以下規則來確定的:
- 越具體的模板優先級越高
- 包含更多匹配段的模板更具體
- 含有文本的段比參數段更具體
- 具有約束的參數段比沒有約束的參數段更具體
- 復雜段和具有約束的段同樣具體
catch-all
參數段是最不具體的
核心源碼解析
AddRouting
public static class RoutingServiceCollectionExtensions
{
public static IServiceCollection AddRouting(this IServiceCollection services)
{
// 內聯約束解析器,負責創建 IRouteConstraint 實例
services.TryAddTransient<IInlineConstraintResolver, DefaultInlineConstraintResolver>();
// 對象池
services.TryAddTransient<ObjectPoolProvider, DefaultObjectPoolProvider>();
services.TryAddSingleton<ObjectPool<UriBuildingContext>>(s =>
{
var provider = s.GetRequiredService<ObjectPoolProvider>();
return provider.Create<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy());
});
services.TryAdd(ServiceDescriptor.Transient<TreeRouteBuilder>(s =>
{
var loggerFactory = s.GetRequiredService<ILoggerFactory>();
var objectPool = s.GetRequiredService<ObjectPool<UriBuildingContext>>();
var constraintResolver = s.GetRequiredService<IInlineConstraintResolver>();
return new TreeRouteBuilder(loggerFactory, objectPool, constraintResolver);
}));
// 標記已將所有路由服務注冊完畢
services.TryAddSingleton(typeof(RoutingMarkerService));
var dataSources = new ObservableCollection<EndpointDataSource>();
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<RouteOptions>, ConfigureRouteOptions>(
serviceProvider => new ConfigureRouteOptions(dataSources)));
// EndpointDataSource,用於全局訪問終結點列表
services.TryAddSingleton<EndpointDataSource>(s =>
{
return new CompositeEndpointDataSource(dataSources);
});
services.TryAddSingleton<ParameterPolicyFactory, DefaultParameterPolicyFactory>();
// MatcherFactory,用於根據 EndpointDataSource 創建 Matcher
services.TryAddSingleton<MatcherFactory, DfaMatcherFactory>();
// DfaMatcherBuilder,用於創建 DfaMatcher 實例
services.TryAddTransient<DfaMatcherBuilder>();
services.TryAddSingleton<DfaGraphWriter>();
services.TryAddTransient<DataSourceDependentMatcher.Lifetime>();
services.TryAddSingleton<EndpointMetadataComparer>(services =>
{
return new EndpointMetadataComparer(services);
});
// LinkGenerator相關服務
services.TryAddSingleton<LinkGenerator, DefaultLinkGenerator>();
services.TryAddSingleton<IEndpointAddressScheme<string>, EndpointNameAddressScheme>();
services.TryAddSingleton<IEndpointAddressScheme<RouteValuesAddress>, RouteValuesAddressScheme>();
services.TryAddSingleton<LinkParser, DefaultLinkParser>();
// 終結點選擇、匹配策略相關服務
services.TryAddSingleton<EndpointSelector, DefaultEndpointSelector>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HttpMethodMatcherPolicy>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HostMatcherPolicy>());
services.TryAddSingleton<TemplateBinderFactory, DefaultTemplateBinderFactory>();
services.TryAddSingleton<RoutePatternTransformer, DefaultRoutePatternTransformer>();
return services;
}
public static IServiceCollection AddRouting(
this IServiceCollection services,
Action<RouteOptions> configureOptions)
{
services.Configure(configureOptions);
services.AddRouting();
return services;
}
}
UseRouting
public static class EndpointRoutingApplicationBuilderExtensions
{
private const string EndpointRouteBuilder = "__EndpointRouteBuilder";
public static IApplicationBuilder UseRouting(this IApplicationBuilder builder)
{
VerifyRoutingServicesAreRegistered(builder);
var endpointRouteBuilder = new DefaultEndpointRouteBuilder(builder);
// 將 endpointRouteBuilder 放入共享字典中
builder.Properties[EndpointRouteBuilder] = endpointRouteBuilder;
// 將 endpointRouteBuilder 作為構造函數參數傳入 EndpointRoutingMiddleware
return builder.UseMiddleware<EndpointRoutingMiddleware>(endpointRouteBuilder);
}
private static void VerifyRoutingServicesAreRegistered(IApplicationBuilder app)
{
// 必須先執行了 AddRouting
if (app.ApplicationServices.GetService(typeof(RoutingMarkerService)) == null)
{
throw new InvalidOperationException(Resources.FormatUnableToFindServices(
nameof(IServiceCollection),
nameof(RoutingServiceCollectionExtensions.AddRouting),
"ConfigureServices(...)"));
}
}
}
EndpointRoutingMiddleware
終於到了路由匹配的邏輯了,才是我們應該關注的,重點查看Invoke
:
internal sealed class EndpointRoutingMiddleware
{
private const string DiagnosticsEndpointMatchedKey = "Microsoft.AspNetCore.Routing.EndpointMatched";
private readonly MatcherFactory _matcherFactory;
private readonly ILogger _logger;
private readonly EndpointDataSource _endpointDataSource;
private readonly DiagnosticListener _diagnosticListener;
private readonly RequestDelegate _next;
private Task<Matcher>? _initializationTask;
public EndpointRoutingMiddleware(
MatcherFactory matcherFactory,
ILogger<EndpointRoutingMiddleware> logger,
IEndpointRouteBuilder endpointRouteBuilder,
DiagnosticListener diagnosticListener,
RequestDelegate next)
{
_matcherFactory = matcherFactory ?? throw new ArgumentNullException(nameof(matcherFactory));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_diagnosticListener = diagnosticListener ?? throw new ArgumentNullException(nameof(diagnosticListener));
_next = next ?? throw new ArgumentNullException(nameof(next));
_endpointDataSource = new CompositeEndpointDataSource(endpointRouteBuilder.DataSources);
}
public Task Invoke(HttpContext httpContext)
{
// 已經選擇了終結點,則跳過匹配
var endpoint = httpContext.GetEndpoint();
if (endpoint != null)
{
Log.MatchSkipped(_logger, endpoint);
return _next(httpContext);
}
// 等待 _initializationTask 初始化完成,進行匹配,並流轉到下一個中間件
var matcherTask = InitializeAsync();
if (!matcherTask.IsCompletedSuccessfully)
{
return AwaitMatcher(this, httpContext, matcherTask);
}
// _initializationTask在之前就已經初始化完成了,直接進行匹配任務,並流轉到下一個中間件
var matchTask = matcherTask.Result.MatchAsync(httpContext);
if (!matchTask.IsCompletedSuccessfully)
{
return AwaitMatch(this, httpContext, matchTask);
}
// 流轉到下一個中間件
return SetRoutingAndContinue(httpContext);
static async Task AwaitMatcher(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task<Matcher> matcherTask)
{
var matcher = await matcherTask;
// 路由匹配,選擇終結點
await matcher.MatchAsync(httpContext);
await middleware.SetRoutingAndContinue(httpContext);
}
static async Task AwaitMatch(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task matchTask)
{
await matchTask;
await middleware.SetRoutingAndContinue(httpContext);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Task SetRoutingAndContinue(HttpContext httpContext)
{
// 終結點仍然為空,則匹配失敗
var endpoint = httpContext.GetEndpoint();
if (endpoint == null)
{
Log.MatchFailure(_logger);
}
else
{
// 匹配成功則觸發事件
if (_diagnosticListener.IsEnabled() && _diagnosticListener.IsEnabled(DiagnosticsEndpointMatchedKey))
{
// httpContext對象包含了相關信息
_diagnosticListener.Write(DiagnosticsEndpointMatchedKey, httpContext);
}
Log.MatchSuccess(_logger, endpoint);
}
// 流轉到下一個中間件
return _next(httpContext);
}
private Task<Matcher> InitializeAsync()
{
var initializationTask = _initializationTask;
if (initializationTask != null)
{
return initializationTask;
}
// 此處我刪減了部分線程競爭代碼,因為這不是我們討論的重點
// 此處主要目的是在該Middleware中,確保只初始化_initializationTask一次
var matcher = _matcherFactory.CreateMatcher(_endpointDataSource);
using (ExecutionContext.SuppressFlow())
{
_initializationTask = Task.FromResult(matcher);
}
}
}
上述代碼的核心就是將_endpointDataSource
傳遞給_matcherFactory
,創建matcher
,然后進行匹配matcher.MatchAsync(httpContext)
。ASP.NET Core默認使用的 matcher 類型是DfaMatcher
,DFA(Deterministic Finite Automaton)是一種被稱為“確定有限狀態自動機”的算法,可以從候選終結點列表中查找到匹配度最高的那個終結點。
UseEndpoints
public static class EndpointRoutingApplicationBuilderExtensions
{
public static IApplicationBuilder UseEndpoints(this IApplicationBuilder builder, Action<IEndpointRouteBuilder> configure)
{
VerifyRoutingServicesAreRegistered(builder);
VerifyEndpointRoutingMiddlewareIsRegistered(builder, out var endpointRouteBuilder);
configure(endpointRouteBuilder);
var routeOptions = builder.ApplicationServices.GetRequiredService<IOptions<RouteOptions>>();
foreach (var dataSource in endpointRouteBuilder.DataSources)
{
routeOptions.Value.EndpointDataSources.Add(dataSource);
}
return builder.UseMiddleware<EndpointMiddleware>();
}
private static void VerifyEndpointRoutingMiddlewareIsRegistered(IApplicationBuilder app, out DefaultEndpointRouteBuilder endpointRouteBuilder)
{
// 將 endpointRouteBuilder 從共享字典中取出來,如果沒有,則說明之前沒有調用 UseRouting
if (!app.Properties.TryGetValue(EndpointRouteBuilder, out var obj))
{
var message =
$"{nameof(EndpointRoutingMiddleware)} matches endpoints setup by {nameof(EndpointMiddleware)} and so must be added to the request " +
$"execution pipeline before {nameof(EndpointMiddleware)}. " +
$"Please add {nameof(EndpointRoutingMiddleware)} by calling '{nameof(IApplicationBuilder)}.{nameof(UseRouting)}' inside the call " +
$"to 'Configure(...)' in the application startup code.";
throw new InvalidOperationException(message);
}
endpointRouteBuilder = (DefaultEndpointRouteBuilder)obj!;
// UseRouting 和 UseEndpoints 必須添加到同一個 IApplicationBuilder 實例上
if (!object.ReferenceEquals(app, endpointRouteBuilder.ApplicationBuilder))
{
var message =
$"The {nameof(EndpointRoutingMiddleware)} and {nameof(EndpointMiddleware)} must be added to the same {nameof(IApplicationBuilder)} instance. " +
$"To use Endpoint Routing with 'Map(...)', make sure to call '{nameof(IApplicationBuilder)}.{nameof(UseRouting)}' before " +
$"'{nameof(IApplicationBuilder)}.{nameof(UseEndpoints)}' for each branch of the middleware pipeline.";
throw new InvalidOperationException(message);
}
}
}
EndpointMiddleware
EndpointMiddleware
中間件中包含了很多異常處理和日志記錄代碼,為了方便查看核心邏輯,我都刪除並進行了簡化:
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)
{
// 執行該終結點的委托,並且視該中間件為終端中間件
var requestTask = endpoint.RequestDelegate(httpContext);
if (!requestTask.IsCompletedSuccessfully)
{
return requestTask;
}
return Task.CompletedTask;
}
// 若沒有終結點,則繼續執行下一個中間件
return _next(httpContext);
}
}
總結
說了那么多,最后給大家總結了三張UML類圖:
RoutePattern
EndPoint
Matcher
另外,本文僅僅提到了路由的基本使用方式和原理,如果你想要進行更加深入透徹的了解,推薦閱讀蔣金楠老師的ASP.NET Core 3框架揭秘的路由部分。