理解ASP.NET Core - 路由(Routing)

注:本文隸屬於《理解ASP.NET Core》系列文章,請查看置頂博客或點擊此處查看全文目錄

Routing

  • Routing(路由):更準確的應該叫做Endpoint Routing,負責將HTTP請求按照匹配規則選擇對應的終結點
  • Endpoint(終結點):負責當HTTP請求到達時,執行代碼

路由是通過UseRoutingUseEndpoints兩個中間件配合在一起來完成註冊的:

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中間件選擇的終結點所關聯的委託。簡單說,該中間件用來執行所選擇的終結點委託

UseRoutingUseEndpoints必須同時使用,而且必須先調用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之前,你可以註冊一些用於修改路由操作的數據,比如UseRewriterUseHttpMethodOverrideUsePathBase等。
  • 在調用UseRoutingUseEndpoints之間,可以註冊一些用於提前處理路由結果的中間件,如UseAuthenticationUseAuthorizationUseCors等。

我們一起看下面的代碼:

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}
  • ?作爲參數後綴,以指示該參數是可選的,如:{name?}
  • 通過=設置默認值,如:{name=jjj} 表示name的默認值是jjj
  • 通過:添加內聯約束,如:{id:int},後面追加:可以添加多個內聯約束,如:{id:int:min(1)}
  • 多個路由參數間必須通過文本或分隔符分隔,例如 {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框架揭祕的路由部分。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章