針對終結點的路由是由EndpointRoutingMiddleware和EndpointMiddleware這兩個中間件協同完成的。應用在啟動之前會注冊若干表示終結點的Endpoint對象(具體來說是包含路由模式的RouteEndpoint對象)。如下圖所示,當應用接收到請求並創建HttpContext上下文之后,EndpointRoutingMiddleware中間件會根據請求的URL及其他相關信息從注冊的終結點中選擇匹配度最高的那個。之后被選擇的終結點會以一個特性(Feature)的形式附加到當前HttpContext上下文中,EndpointMiddleware中間件最終提供這個終結點並用它來處理當前請求。[更多關於ASP.NET Core的文章請點這里]
目錄
一、IEndpointFeature
二、EndpointRoutingMiddleware
三、EndpointMiddleware
四、注冊終結點
一、IEndpointFeature
EndpointRoutingMiddleware中間件選擇的終結點會以特性的形式存放在當前HttpContext上下文中,這個用來封裝終結點的特性通過IEndpointFeature接口表示。如下面的代碼片段所示,IEndpointFeature接口通過唯一的屬性Endpoint表示針對當前請求選擇的終結點。我們可以調用HttpContext類型的GetEndpoint方法和SetEndpoint方法來獲取與設置用來處理當前請求的終結點。
public interface IEndpointFeature { Endpoint Endpoint { get; set; } } public static class EndpointHttpContextExtensions { public static Endpoint GetEndpoint(this HttpContext context) =>context.Features.Get<IEndpointFeature>()?.Endpoint; public static void SetEndpoint(this HttpContext context, Endpoint endpoint) { var feature = context.Features.Get<IEndpointFeature>(); if (feature != null) { feature.Endpoint = endpoint; } else { context.Features.Set<IEndpointFeature>(new EndpointFeature { Endpoint = endpoint }); } } private class EndpointFeature : IEndpointFeature { public Endpoint Endpoint { get; set; } } }
二、EndpointRoutingMiddleware
EndpointRoutingMiddleware中間件利用一個Matcher對象選擇出與當前HttpContext上下文相匹配的終結點,然后將選擇的終結點以IEndpointFeature特性的形式附加到當前HttpContext上下文中。Matcher只是一個內部抽象類型,針對終結點的選擇和設置實現在它的MatchAsync方法中。如果匹配的終結點被成功選擇出來,MatchAsync方法還會提取出解析出來的路由參數,然后將它們逐個添加到表示當前請求的HttpRequest對象的RouteValues屬性字典中。
internal abstract class Matcher { public abstract Task MatchAsync(HttpContext httpContext); } public abstract class HttpRequest { public virtual RouteValueDictionary RouteValues { get; set; } } public class RouteValueDictionary : IDictionary<string, object>, IReadOnlyDictionary<string, object> { ... }
EndpointRoutingMiddleware中間件使用的Matcher由注冊的MatcherFactory服務來提供。路由系統默認使用的Matcher類型為DfaMatcher,它采用一種被稱為確定有限狀態自動機(Deterministic Finite Automaton,DFA)的形式從候選終結點中找到與當前請求匹配度最高的那個。由於篇幅有限,具體的細節此處不再展開介紹。DfaMatcher最終會利用DfaMatcherFactory對象間接地創建出來,DfaMatcherFactory類型派生於抽象類MatcherFactory。
internal abstract class MatcherFactory { public abstract Matcher CreateMatcher(EndpointDataSource dataSource); }
對Matcher和MatcherFactory有了基本了解之后,我們將關注點轉移到EndpointRoutingMiddleware中間件。如下所示的代碼片段模擬了EndpointRoutingMiddleware中間件的實現邏輯。我們在構造函數中注入了用於提供注冊終結點的IEndpointRouteBuilder對象和用來創建Matcher對象的MatcherFactory工廠。
internal class EndpointRoutingMiddleware { private readonly RequestDelegate _next; private readonly Task<Matcher> _matcherAccessor; public EndpointRoutingMiddleware(RequestDelegate next, IEndpointRouteBuilder builder, MatcherFactory factory) { _next = next; _matcherAccessor = new Task<Matcher>(CreateMatcher); Matcher CreateMatcher() { var source = new CompositeEndpointDataSource(builder.DataSources); return factory.CreateMatcher(source); } } public async Task InvokeAsync(HttpContext httpContext) { var matcher = await _matcherAccessor; await matcher.MatchAsync(httpContext); await _next(httpContext); } }
在實現的InvokeAsync方法中,我們只需要根據IEndpointRouteBuilder對象提供的終結點列表創建一個CompositeEndpointDataSource對象,並將其作為參數調用MatcherFactory工廠的CreateMatcher方法。該方法會返回一個Matcher對象,然后調用Matcher對象的MatchAsync方法選擇出匹配的終結點,並以特性的方式附加到當前HttpContext上下文中。EndpointRoutingMiddleware中間件一般通過如下所示的UseRouting擴展方法進行注冊。
public static class EndpointRoutingApplicationBuilderExtensions { public static IApplicationBuilder UseRouting(this IApplicationBuilder builder); }
三、EndpointMiddleware
EndpointMiddleware中間件的職責特別明確,就是執行由EndpointRoutingMiddleware中間件附加到當前HttpContext上下文中的終結點。EndpointRoutingMiddleware中間件針對終結點的執行涉及如下所示的RouteOptions類型標識的配置選項。
public class RouteOptions { public bool LowercaseUrls { get; set; } public bool LowercaseQueryStrings { get; set; } public bool AppendTrailingSlash { get; set; } public IDictionary<string, Type> ConstraintMap { get; set; } public bool SuppressCheckForUnhandledSecurityMetadata { get; set; } }
配置選項RouteOptions的前三個屬性與路由系統針對URL的生成有關。具體來說,LowercaseUrls屬性和LowercaseQueryStrings屬性決定是否會將生成的URL或者查詢字符串轉換成小寫形式。AppendTrailingSlash屬性則決定是否會為生成的URL添加后綴“/”。RouteOptions的ConstraintMap屬性表示的字典與路由參數的內聯約束有關,它提供了在路由模板中實現的約束字符串(如regex表示正則表達式約束)與對應約束類型(正則表達式約束類型為RegexRouteConstraint)之間的映射關系。
真正與EndpointMiddleware中間件相關的是RouteOptions的SuppressCheckForUnhandledSecurityMetadata屬性,它表示目標終結點利用添加的元數據設置了一些關於安全方面的要求(主要是授權和跨域資源共享方面的要求),但是目前的請求並未經過相應的中間件處理(通過請求是否具有要求的報頭判斷),在這種情況下是否還有必要繼續執行目標終結點。如果這個屬性設置為True,就意味着EndpointMiddleware中間件根本不會做這方面的檢驗。如下所示的代碼片段模擬了EndpointMiddleware中間件對請求的處理邏輯。
internal class EndpointMiddleware { private readonly RequestDelegate _next; private readonly RouteOptions _options; public EndpointMiddleware(RequestDelegate next, IOptions<RouteOptions> optionsAccessor) { _next = next; _options = optionsAccessor.Value; } public Task InvokeAsync(HttpContext httpContext) { var endpoint = httpContext.GetEndpoint(); if (null != endpoint) { if (!_options.SuppressCheckForUnhandledSecurityMetadata) { CheckSecurity(); } return endpoint.RequestDelegate(httpContext); } return _next(httpContext); } private void CheckSecurity(); }
我們一般調用如下所示的UseEndpoints擴展方法來注冊EndpointMiddleware中間件,該方法提供了一個類型為Action<IEndpointRouteBuilder>的參數。通過前面的介紹可知,EndpointRoutingMiddleware中間件會利用注入的IEndpointRouteBuilder對象來獲取注冊的表示終結點數據源的EndpointDataSource,所以可以通過這個方法為EndpointRoutingMiddleware中間件注冊終結點數據源。
public static class EndpointRoutingApplicationBuilderExtensions { public static IApplicationBuilder UseEndpoints(this IApplicationBuilder builder, Action<IEndpointRouteBuilder> configure); }
四、注冊終結點
對於使用路由系統的應用程序來說,它的主要工作基本集中在針對EndpointDataSource的注冊上。一般來說,當我們調用IApplicationBuilder接口的UseEndpoints擴展方法注冊EndpointMiddleware中間件時,會利用提供的Action<IEndpointRouteBuilder>委托對象注冊所需的EndpointDataSource對象。IEndpointRouteBuilder接口具有一系列的擴展方法,這些方法可以幫助我們注冊所需的終結點。
如下所示的Map方法會根據提供的作為路由模式和處理器的RoutePattern對象與RequestDelegate對象創建一個終結點,並以ModelEndpointDataSource的形式予以注冊。如下所示的代碼片段還揭示了一個細節:對於作為請求處理器的RequestDelegate委托對象來說,其對應方法上標注的所有特性會以元數據的形式添加到創建的終結點上。
public static class EndpointRouteBuilderExtensions { public static IEndpointConventionBuilder Map(this IEndpointRouteBuilder endpoints, RoutePattern pattern, RequestDelegate requestDelegate) { var builder = new RouteEndpointBuilder(requestDelegate, pattern, 0) { DisplayName = pattern.RawText }; var attributes = requestDelegate.Method.GetCustomAttributes(); if (attributes != null) { foreach (var attribute in attributes) { builder.Metadata.Add(attribute); } } var dataSource = endpoints.DataSources.OfType<ModelEndpointDataSource>().FirstOrDefault()?? new ModelEndpointDataSource(); endpoints.DataSources.Add(dataSource); return dataSource.AddEndpointBuilder(builder); } }
HTTP方法(Method)在RESTful API的設計中具有重要意義,幾乎所有的終結點都會根據自身對資源的操作類型對請求采用HTTP方法做相應限制。如果需要為注冊的終結點指定限定的HTTP方法,就可以調用如下所示的MapMethods方法。該方法會在Map方法的基礎上為注冊的終結點設置相應的顯示名稱,並針對指定的HTTP方法創建一個HttpMethodMetadata對象,然后作為元數據添加到注冊的終結點上。
public static class EndpointRouteBuilderExtensions { public static IEndpointConventionBuilder MapMethods(this IEndpointRouteBuilder endpoints, string pattern, IEnumerable<string> httpMethods, RequestDelegate requestDelegate) { var builder = endpoints.Map(RoutePatternFactory.Parse(pattern), requestDelegate); builder.WithDisplayName($"{pattern} HTTP: {string.Join(", ", httpMethods)}"); builder.WithMetadata(new HttpMethodMetadata(httpMethods)); return builder; } }
EndpointRoutingMiddleware中間件在為當前請求篩選匹配的終結點時,針對HTTP方法的選擇策略是通過IHttpMethodMetadata接口表示的元數據指定的,HttpMethodMetadata類型正是對該接口的默認實現。如下面的代碼片段所示,IHttpMethodMetadata接口除了具有一個表示可接受HTTP方法列表的HttpMethods屬性,還有一個布爾類型的只讀屬性AcceptCorsPreflight,它表示是否接受針對跨域資源共享(Cross-Origin Resource Sharing,CORS)的預檢(Preflight)請求。
public interface IHttpMethodMetadata { IReadOnlyList<string> HttpMethods { get; } bool AcceptCorsPreflight { get; } } public sealed class HttpMethodMetadata : IHttpMethodMetadata { public IReadOnlyList<string> HttpMethods { get; } public bool AcceptCorsPreflight { get; } public HttpMethodMetadata(IEnumerable<string> httpMethods): this(httpMethods, acceptCorsPreflight: false) {} public HttpMethodMetadata(IEnumerable<string> httpMethods, bool acceptCorsPreflight) { HttpMethods = httpMethods.ToArray(); AcceptCorsPreflight = acceptCorsPreflight; } }
路由系統還為4種常用的HTTP方法(GET、POST、PUT和DELETE)定義了相應的方法。從如下所示的代碼片段可以看出,它們最終調用的都是MapMethods方法。我們在本章開篇演示的實例中正是調用其中的MapGet方法來注冊終結點的。
public static class EndpointRouteBuilderExtensions { public static IEndpointConventionBuilder MapGet(this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate) => MapMethods(endpoints, pattern, "GET", requestDelegate); public static IEndpointConventionBuilder MapPost(this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate) => MapMethods(endpoints, pattern, "POST", requestDelegate); public static IEndpointConventionBuilder MapPut(this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate) => MapMethods(endpoints, pattern, "PUT", requestDelegate); public static IEndpointConventionBuilder MapDelete(this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate) => MapMethods(endpoints, pattern, "DELETE", requestDelegate); }
調用IApplicationBuilder接口相應的擴展方法注冊EndpointRoutingMiddleware中間件和EndpointMiddleware中間件時,必須確保它們依賴的服務已經被注冊到依賴注入框架之中。針對路由服務的注冊可以通過調用如下所示的AddRouting擴展方法重載來完成。
public static class RoutingServiceCollectionExtensions { public static IServiceCollection AddRouting(this IServiceCollection services); public static IServiceCollection AddRouting(this IServiceCollection services, Action<RouteOptions> configureOptions); }
ASP.NET Core路由中間件[1]: 終結點與URL的映射
ASP.NET Core路由中間件[2]: 路由模式
ASP.NET Core路由中間件[3]: 終結點
ASP.NET Core路由中間件[4]: EndpointRoutingMiddleware和EndpointMiddleware
ASP.NET Core路由中間件[5]: 路由約束