ASP.NET Core路由中間件[3]: 終結點(Endpoint)

到目前為止,ASP.NET Core提供了兩種不同的路由解決方案。傳統的路由系統以IRouter對象為核心,我們姑且將其稱為IRouter路由。本章介紹的是最早發佈於ASP.NET Core 2.2中的新路由系統,由於它採用基於終結點映射的策略,所以我們將其稱為終結點路由。終結點路由自然以終結點為核心,所以先介紹終結點在路由系統中的表現形式。[更多關於ASP.NET Core的文章請點這裡]

之所以將應用劃分為若干不同的終結點,是因為不同的終結點具有不同的請求處理方式。ASP.NET Core應用可以利用RequestDelegate對象來表示HTTP請求處理器,每個終結點都封裝了一個RequestDelegate對象並用它來處理路由給它的請求。如下圖所示,除了請求處理器,終結點還提供了一個用來存放元數據的容器,路由過程中的很多行為都可以通過相應的元數據來控制。

15-9

一、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對象是路由註冊的一項核心工作。

15-10

如下面的程式碼片段所示,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對象上。

15-20

除了上述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為核心的終結點模型體現在下圖中。

15-12

ASP.NET Core路由中間件[1]: 終結點與URL的映射
ASP.NET Core路由中間件[2]: 路由模式
ASP.NET Core路由中間件[3]: 終結點
ASP.NET Core路由中間件[4]: EndpointRoutingMiddleware和EndpointMiddleware
ASP.NET Core路由中間件[5]: 路由約束