17

ASP.NET Core路由中间件[3]: 终结点(Endpoint)

 4 years ago
source link: https://www.cnblogs.com/artech/p/endpoint-middleware-03.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

到目前为止,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]: 路由约束


Recommend

  • 18

    ASP.NET Core应用默认的请求处理管道是由注册的IServer对象和HostingApplication对象组成的,后者利用一个在创建时提供的RequestDelegate对象来处理IServer对象分发给它的请求。而RequestDelegate对象实际上是由所有的中间件按照注册顺序创建的。换句话说,这个...

  • 22

    写在前面 上一篇大家已经粗略接触了解到.NET Core中间件的使用:ASP .Net Core 中间件的使用(一):搭建静态文件服务器/访问指定文件,

  • 37

    借助路由系统提供的请求URL模式与对应终结点(Endpoint)之间的映射关系,我们可以将具有相同URL模式的请求分发给应用的终结点进行处理。ASP.NET Core的路由是通过EndpointRoutingMiddleware和EndpointMiddleware这两个中间件协作完成的...

  • 20

    ASP.NET Core路由中间件[2]: 路由模式 一个Web应用本质上体现为...

  • 7

    表示路由终结点的RouteEndpoint对象包含以RoutePattern对象表示的路由模式,某个请求能够被成功路由的前提是它满足某个候选终结点的路由模式所体现的路由规则。具体来说,这不仅要求当前请求的URL路径必须满足路由模板指定的路径模式,还需要具体的字符内容满足...

  • 6
    • www.daqianduan.com 4 years ago
    • Cache

    ASP.NET Core 3.1 中间件

    参考微软官方文档 : https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1 一、ASP.NET Core 中间件简介 1.1 定义:中间件是一种装配到应用管道以...

  • 8

    ASP.NET Core路由中间件[4]: EndpointRoutingMiddleware和EndpointMiddleware ...

  • 13

    ASP.Net Core 是微软开源的跨平台、可扩展、轻量级的模块化框架,可用于构建高性能的web应用程序。中间件组件可以注入到 ASP.Net Core 请求管道中...

  • 16

    具有跨平台能力的KestrelServer是最重要的服务器类型。针对KestrelServer的设置均体现在KestrelServerOptions配置选项上,注册的终结点是它承载的最重要的配置选项。这里所谓的终结点(Endpoint)与“路由”介绍的终结点不是一回事,这里表示的就是服务器在监听请...

  • 4

    【Azure 应用服务】App Service 开启了私有终结点(Private Endpoint)模式后,如何来实现公网Git部署呢? ...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK