到目前為止,ASP.NET Core提供了兩種不同的路由解決方案。傳統的路由系統以IRouter對象為核心,我們姑且將其稱為IRouter路由。本章介紹的是最早發布於ASP.NET Core 2.2中的新路由系統,由於它采用基於終結點映射的策略,所以我們將其稱為終結點路由。終結點路由自然以終結點為核心,所以先介紹終結點在路由系統中的表現形式。[更多關於ASP.NET Core的文章請點這里]
之所以將應用划分為若干不同的終結點,是因為不同的終結點具有不同的請求處理方式。ASP.NET Core應用可以利用RequestDelegate對象來表示HTTP請求處理器,每個終結點都封裝了一個RequestDelegate對象並用它來處理路由給它的請求。如下圖所示,除了請求處理器,終結點還提供了一個用來存放元數據的容器,路由過程中的很多行為都可以通過相應的元數據來控制。
一、Endpoint & EndpointBuilder
路由系統中的終結點通過如下所示的Endpoint類型表示。組成終結點的兩個核心成員(請求處理器和元數據集合)分別體現為只讀屬性RequestDelegate和Metadata。除此之外,終結點還有一個顯示名稱的只讀屬性DisplayName。
public class Endpoint { public string DisplayName { get; } public RequestDelegate RequestDelegate { get; } public EndpointMetadataCollection Metadata { get; } public Endpoint(RequestDelegate requestDelegate, EndpointMetadataCollection metadata, string displayName); }
終結點元數據集合體現為一個EndpointMetadataCollection對象。由於終結點並未對元數據的形式做任何限制,原則上任何對象都可以作為終結點的元數據,所以EndpointMetadataCollection對象本質上就是一個元素類型為Object的集合。如下面的代碼片段所示,EndpointMetadata
Collection對象是一個只讀列表,它包含的元數據需要在該集合被創建時被提供。
public sealed class EndpointMetadataCollection : IReadOnlyList<object> { public object this[int index] { get; } public int Count { get; } public EndpointMetadataCollection(IEnumerable<object> items); public EndpointMetadataCollection(params object[] items); public Enumerator GetEnumerator(); public T GetMetadata<T>() where T: class; public IReadOnlyList<T> GetOrderedMetadata<T>() where T: class; IEnumerator<object> IEnumerable<object>.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator(); }
我們可以調用泛型方法GetMetadata<T>得到指定類型的元數據,由於多個具有相同類型的元數據可能會被添加到集合中,所以這個方法會采用“后來居上”的策略,返回最后被添加的元數據對象。如果沒有指定類型的元數據,該方法會返回指定類型的默認值。如果希望按序返回指定類型的所有元數據,可以調用另一個泛型方法GetOrderedMetadata<T>。
路由系統利用EndpointBuilder來構建表示終結點的Endpoint對象。如下面的代碼片段所示,EndpointBuilder是一個抽象類,針對終結點的構建體現在抽象的Build方法中。EndpointBuilder定義了對應的屬性來設置終結點的請求處理器、元數據和顯示名稱。
public abstract class EndpointBuilder { public RequestDelegate RequestDelegate { get; set; } public string DisplayName { get; set; } public IList<object> Metadata { get; } public abstract Endpoint Build(); }
二、RouteEndpoint & RouteEndpointBuilder
路由系統的終結點體現為一個RouteEndpoint對象,它實際上是將映射的路由模式融入終結點中。如下面的代碼片段所示,派生於Endpoint的RouteEndpoint類型有一個名為RoutePattern的只讀屬性,返回的正是表示路由模式的RoutePattern對象。除此之外,RouteEndpoint類型還有另一個表示注冊順序的Order屬性。
public sealed class RouteEndpoint : Endpoint { public RoutePattern RoutePattern { get; } public int Order { get; } public RouteEndpoint(RequestDelegate requestDelegate, RoutePattern routePattern, int order, EndpointMetadataCollection metadata, string displayName); }
RouteEndpoint對象由RouteEndpointBuilder構建而成。如下面的代碼片段所示,RouteEndpoint
Builder類型派生於抽象基類EndpointBuilder。在重寫的Build方法中,RouteEndpointBuilder類型根據構造函數或者屬性指定的信息創建出返回的RouteEndpoint對象。
public sealed class RouteEndpointBuilder : EndpointBuilder { public RoutePattern RoutePattern { get; set; } public int Order { get; set; } public RouteEndpointBuilder(RequestDelegate requestDelegate, RoutePattern routePattern, int order) { base.RequestDelegate = requestDelegate; RoutePattern = routePattern; Order = order; } public override Endpoint Build() => new RouteEndpoint(base.RequestDelegate, RoutePattern, Order, new EndpointMetadataCollection((IEnumerable<object>)base.Metadata), base.DisplayName); }
三、EndpointDataSource
路由系統中的終結點體現了針對某類請求的處理方式,它們的來源具有不同的表現形式,終結點的數據源通過EndpointDataSource表示。如下圖所示,一個EndpointDataSource對象可以提供多個表示終結點的Endpoint對象,為應用提供相應的EndpointDataSource對象是路由注冊的一項核心工作。
如下面的代碼片段所示,EndpointDataSource是一個抽象類,除了表示提供終結點列表的只讀屬性Endpoints,它還提供了一個GetChangeToken方法,我們可以利用這個方法返回的IChangeToken對象來感知數據源的變化。
public abstract class EndpointDataSource { public abstract IReadOnlyList<Endpoint> Endpoints { get; } public abstract IChangeToken GetChangeToken(); }
路由系統提供了一個DefaultEndpointDataSource類型。如下面的代碼片段所示,Default
EndpointDataSource通過重寫的Endpoints屬性提供的終結點列表在構造函數中是顯式指定的,其GetChangeToken方法返回的是一個不具有感知能力的NullChangeToken對象。
public sealed class DefaultEndpointDataSource : EndpointDataSource { private readonly IReadOnlyList<Endpoint> _endpoints; public override IReadOnlyList<Endpoint> Endpoints => _endpoints; public DefaultEndpointDataSource(IEnumerable<Endpoint> endpoints) =>_endpoints = (IReadOnlyList<Endpoint>) new List<Endpoint>(endpoints); public DefaultEndpointDataSource(params Endpoint[] endpoints) =>_endpoints = (Endpoint[]) endpoints.Clone(); public override IChangeToken GetChangeToken() => NullChangeToken.Singleton; }
對於本章開篇演示的一系列路由實例來說,我們最終注冊的實際上是一個類型為ModelEndpointDataSource的終結點數據源,它依然是一個未被公開的內部類型。要理解ModelEndpointDataSource針對終結點的提供機制,就必須了解另一個名為 IEndpointConventionBuilder的接口。顧名思義,IEndpointConventionBuilder體現了一種針對“約定”的終結點構建方式。
如下面的代碼片段所示,該接口定義了一個唯一的Add方法,針對終結點構建的約定體現在該方法類型為Action<EndpointBuilder>的參數上。IEndpointConventionBuilder接口還有如下所示的3個擴展方法,用來為構建的終結點設置顯示名稱和元數據。
public interface IEndpointConventionBuilder { void Add(Action<EndpointBuilder> convention); } public static class RoutingEndpointConventionBuilderExtensions { public static TBuilder WithDisplayName<TBuilder>(this TBuilder builder, Func<EndpointBuilder, string> func) where TBuilder : IEndpointConventionBuilder { builder.Add(it=>it.DisplayName = func(it)); return builder; } public static TBuilder WithDisplayName<TBuilder>(this TBuilder builder, string displayName) where TBuilder : IEndpointConventionBuilder { builder.Add(it => it.DisplayName = displayName); return builder; } public static TBuilder WithMetadata<TBuilder>(this TBuilder builder, params object[] items) where TBuilder : IEndpointConventionBuilder { builder.Add(it => Array.ForEach(items, item => it.Metadata.Add(item))); return builder; } }
ModelEndpointDataSource這個終結點數據源內部會使用一個名為DefaultEndpointConventionBuilder的類型,如下所示的代碼片段給出了這兩個類型的完整實現。從給出的代碼片段可以看出,ModelEndpointDataSource的GetChangeToken方法返回的依然是一個不具有感知能力的NullChangeToken對象。
internal class DefaultEndpointConventionBuilder : IEndpointConventionBuilder { private readonly List<Action<EndpointBuilder>> _conventions; internal EndpointBuilder EndpointBuilder { get; } public DefaultEndpointConventionBuilder(EndpointBuilder endpointBuilder) { EndpointBuilder = endpointBuilder; _conventions = new List<Action<EndpointBuilder>>(); } public void Add(Action<EndpointBuilder> convention) =>_conventions.Add(convention); public Endpoint Build() { foreach (var convention in _conventions) { convention(EndpointBuilder); } return EndpointBuilder.Build(); } } internal class ModelEndpointDataSource : EndpointDataSource { private List<DefaultEndpointConventionBuilder> _endpointConventionBuilders; public ModelEndpointDataSource() => _endpointConventionBuilders = new List<DefaultEndpointConventionBuilder>(); public IEndpointConventionBuilder AddEndpointBuilder(EndpointBuilder endpointBuilder) { var builder = new DefaultEndpointConventionBuilder(endpointBuilder); _endpointConventionBuilders.Add(builder); return builder; } public override IChangeToken GetChangeToken()=> NullChangeToken.Singleton; public override IReadOnlyList<Endpoint> Endpoints => _endpointConventionBuilders.Select(it => it.Build()).ToArray(); }
綜上所示,ModelEndpointDataSource最終采用下圖所示的方式來提供終結點。當我們調用其AddEndpointBuilder方法為它添加一個EndpointBuilder對象時,它會利用這個EndpointBuilder對象創建一個DefaultEndpointConventionBuilder對象。DefaultEndpointConventionBuilder針對終結點的構建最終還是落在EndpointBuilder對象上。
除了上述ModelEndpointDataSource/DefaultEndpointConventionBuilder類型,ASP.NET Core MVC和Razor Pages框架分別根據自身的路由約定提供了針對EndpointDataSource和IEndpointConventionBuilder的實現。路由系統還提供了如下所示的CompositeEndpointDataSource類型。顧名思義,一個CompositeEndpointDataSource對象實際上是對一組EndpointDataSource對象的組合,它重寫的Endpoints屬性返回的終結點由作為組成成員的EndpointDataSource對象共同提供。它的GetChangeToken方法返回的IChangeToken對象可以幫助我們感知其中任何一個EndpointDataSource對象的改變。
public sealed class CompositeEndpointDataSource : EndpointDataSource { public IEnumerable<EndpointDataSource> DataSources { get; } public override IReadOnlyList<Endpoint> Endpoints { get; } public CompositeEndpointDataSource(IEnumerable<EndpointDataSource> endpointDataSources); public override IChangeToken GetChangeToken(); }
四、IEndpointRouteBuilder
表示終結點數據源的EndpointDataSource對象是借助IEndpointRouteBuilder對象注冊的。我們可以在一個IEndpointRouteBuilder對象上注冊多個EndpointDataSource對象,它們會被添加到DataSources屬性表示的集合中。IEndpointRouteBuilder接口還通過只讀屬性ServiceProvider提供了作為依賴注入容器的IServiceProvider對象。
public interface IEndpointRouteBuilder { ICollection<EndpointDataSource> DataSources { get; } IServiceProvider ServiceProvider { get; } IApplicationBuilder CreateApplicationBuilder(); }
IEndpointRouteBuilder接口的CreateApplicationBuilder方法會幫助我們創建一個新的IApplicationBuilder對象。如果某個終結點針對請求處理的邏輯相對復雜,需要多個終結點協同完成,就可以將這些中間件注冊到這個IApplicationBuilder對象上,然后利用它創建的Request
Delegate對象來處理路由的請求。如下所示的內部類型DefaultEndpointRouteBuilder是對IEndpointRouteBuilder接口的默認實現。
internal class DefaultEndpointRouteBuilder : IEndpointRouteBuilder { public ICollection<EndpointDataSource> DataSources { get; } public IServiceProvider ServiceProvider => ApplicationBuilder.ApplicationServices; public IApplicationBuilder ApplicationBuilder { get; } public DefaultEndpointRouteBuilder(IApplicationBuilder applicationBuilder) { ApplicationBuilder = applicationBuilder; DataSources = new List<EndpointDataSource>(); } public IApplicationBuilder CreateApplicationBuilder() => ApplicationBuilder.New(); }
本節的內容以終結點為核心,表示終結點的Endpoint對象來源於通過EndpointDataSource對象表示的數據源,EndpointDataSource對象注冊到IEndpointRouteBuilder對象上。以IEndpointRouteBuilder、EndpointDataSource和Endpoint為核心的終結點模型體現在下圖中。
ASP.NET Core路由中間件[1]: 終結點與URL的映射
ASP.NET Core路由中間件[2]: 路由模式
ASP.NET Core路由中間件[3]: 終結點
ASP.NET Core路由中間件[4]: EndpointRoutingMiddleware和EndpointMiddleware
ASP.NET Core路由中間件[5]: 路由約束