//這是重點,是ASP.NET Core自身提供的
builder.Services.AddEndpointsApiExplorer();
//添加swagger配置
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new()
{
Title = builder.Environment.ApplicationName,
Version = "v1"
});
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
//swagger終結(jié)點
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json",
$"{builder.Environment.ApplicationName} v1"));
}

app.MapGet("/swag", () => "Hello Swagger!");

app.Run();

上面我們提到了AddEndpointsApiExplorer是ASP.NET Core自身提供的,但是如果使得MinimalApi能在Swagger中展示就必須要添加這個服務(wù)。所以Swagger還是那個Swagger,變的是ASP.NET Core本身,但是變化是如何適配數(shù)據(jù)源的問題,Swagger便是建立在這個便利基礎(chǔ)上。接下來咱們就通過源碼看一下它們之間的關(guān)系。

源碼探究

想了解它們的關(guān)系就會涉及到兩個主角,一個是swagger的數(shù)據(jù)源來自何處,另一個是ASP.NET Core是如何提供這個數(shù)據(jù)源的。首先我們來看一下Swagger的數(shù)據(jù)源來自何處。

swagger的數(shù)據(jù)源

熟悉Swashbuckle.AspNetCore的應(yīng)該知道它其實是由幾個程序集一起構(gòu)建的,也就是說Swashbuckle.AspNetCore本身是一個解決方案,不過這不是重點,其中生成Swagger.json的是在Swashbuckle.AspNetCore.SwaggerGen程序集中,直接找到位置在SwaggerGenerator類中只摘要我們關(guān)注的地方即可

public class SwaggerGenerator : ISwaggerProvider
{
private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionsProvider;
private readonly ISchemaGenerator _schemaGenerator;
private readonly SwaggerGeneratorOptions _options;

public SwaggerGenerator(
SwaggerGeneratorOptions options,
IApiDescriptionGroupCollectionProvider apiDescriptionsProvider,
ISchemaGenerator schemaGenerator)
{
_options = options ?? new SwaggerGeneratorOptions();
_apiDescriptionsProvider = apiDescriptionsProvider;
_schemaGenerator = schemaGenerator;
}

/// <summary>
/// 獲取Swagger文檔的核心方法
/// </summary>
public OpenApiDocument GetSwagger(string documentName, string host = null, string basePath = null)
{
if (!_options.SwaggerDocs.TryGetValue(documentName, out OpenApiInfo info))
throw new UnknownSwaggerDocument(documentName, _options.SwaggerDocs.Select(d => d.Key));

//組裝OpenApiDocument核心數(shù)據(jù)源源來自_apiDescriptionsProvider
var applicableApiDescriptions = _apiDescriptionsProvider.ApiDescriptionGroups.Items
.SelectMany(group => group.Items)
.Where(apiDesc => !(_options.IgnoreObsoleteActions && apiDesc.CustomAttributes().OfType<ObsoleteAttribute().Any()))
.Where(apiDesc => _options.DocInclusionPredicate(documentName, apiDesc));

var schemaRepository = new SchemaRepository(documentName);

var swaggerDoc = new OpenApiDocument
{
Info = info,
Servers = GenerateServers(host, basePath),
// Paths組裝是來自applicableApiDescriptions
Paths = GeneratePaths(applicableApiDescriptions, schemaRepository),
Components = new OpenApiComponents
{
Schemas = schemaRepository.Schemas,
SecuritySchemes = new Dictionary<string, OpenApiSecurityScheme>(_options.SecuritySchemes)
},
SecurityRequirements = new List<OpenApiSecurityRequirement>(_options.SecurityRequirements)
};

//省略其他代碼
return swaggerDoc;
}
}

如果你比較了解Swagger.json的話那么對OpenApiDocument這個類的結(jié)構(gòu)一定是一目了然,不信的話你可以自行看看它的結(jié)構(gòu)

{
"openapi": "3.0.1",
"info": {
"title": "MyTest.WebApi",
"description": "測試接口",
"version": "v1"
},
"paths": {
"/": {
"get": {
"tags": [
"MyTest.WebApi"
],
"responses": {
"200": {
"description": "Success",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
}
}
}
}
},
"components": {}
}

這么看清晰了吧OpenApiDocument這個類就是返回Swagger.json的模型類,而承載描述接口信息的核心字段paths正是來自IApiDescriptionGroupCollectionProvider。所以小結(jié)一下,Swagger接口的文檔信息的數(shù)據(jù)源來自于IApiDescriptionGroupCollectionProvider

ASP.Net Core如何提供

通過上面在Swashbuckle.AspNetCore.SwaggerGen程序集中,我們看到了真正組裝Swagger接口文檔部分的數(shù)據(jù)源來自于IApiDescriptionGroupCollectionProvider,但是這個接口并非來自Swashbuckle而是來自ASP.NET Core。這就引入了另一個主角,也是我們上面提到的AddEndpointsApiExplorer方法。直接在dotnet/aspnetcore倉庫里找到方法位置看一下方法實現(xiàn)

public static IServiceCollection AddEndpointsApiExplorer(this IServiceCollection services)
{
services.TryAddSingleton<IActionDescriptorCollectionProvider, DefaultActionDescriptorCollectionProvider>();
//swagger用到的核心操作IApiDescriptionGroupCollectionProvider
services.TryAddSingleton<IApiDescriptionGroupCollectionProvider, ApiDescriptionGroupCollectionProvider>();
services.TryAddEnumerable(
ServiceDescriptor.Transient<IApiDescriptionProvider, EndpointMetadataApiDescriptionProvider>());
return services;
}

看到了AddEndpointsApiExplorer方法相信就明白了為啥要添加這個方法了吧,那你就有疑問了為啥不使用MinimalApi的時候就不用引入AddEndpointsApiExplorer這個方法了,況且也能使用swagger。這是因為在AddControllers方法里添加了AddApiExplorer方法,這個方法里包含了針對Controller的接口描述信息,這里就不過多說了,畢竟這種的核心是MinimalApi。接下來就看下IApiDescriptionGroupCollectionProvider接口的默認實現(xiàn)ApiDescriptionGroupCollectionProvider類里的實現(xiàn)

public class ApiDescriptionGroupCollectionProvider : IApiDescriptionGroupCollectionProvider
{
private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;
private readonly IApiDescriptionProvider[] _apiDescriptionProviders;
private ApiDescriptionGroupCollection? _apiDescriptionGroups;

public ApiDescriptionGroupCollectionProvider(
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider,
IEnumerable<IApiDescriptionProvider> apiDescriptionProviders)
{
_actionDescriptorCollectionProvider = actionDescriptorCollectionProvider;
_apiDescriptionProviders = apiDescriptionProviders.OrderBy(item => item.Order).ToArray();
}

public ApiDescriptionGroupCollection ApiDescriptionGroups
{
get
{
var actionDescriptors = _actionDescriptorCollectionProvider.ActionDescriptors;
if (_apiDescriptionGroups == null || _apiDescriptionGroups.Version != actionDescriptors.Version)
{
//如果_apiDescriptionGroups為null則使用GetCollection方法返回的數(shù)據(jù)
_apiDescriptionGroups = GetCollection(actionDescriptors);
}
return _apiDescriptionGroups;
}
}

private ApiDescriptionGroupCollection GetCollection(ActionDescriptorCollection actionDescriptors)
{
var context = new ApiDescriptionProviderContext(actionDescriptors.Items);

//這里使用了_apiDescriptionProviders
foreach (var provider in _apiDescriptionProviders)
{
provider.OnProvidersExecuting(context);
}

for (var i = _apiDescriptionProviders.Length - 1; i >= 0; i--)
{
_apiDescriptionProviders[i].OnProvidersExecuted(context);
}

var groups = context.Results
.GroupBy(d => d.GroupName)
.Select(g => new ApiDescriptionGroup(g.Key, g.ToArray()))
.ToArray();
return new ApiDescriptionGroupCollection(groups, actionDescriptors.Version);
}
}

這里我們看到了IApiDescriptionProvider[]通過上面的方法我們可以知道IApiDescriptionProvider默認實現(xiàn)是EndpointMetadataApiDescriptionProvider類看一下相實現(xiàn)

internal class EndpointMetadataApiDescriptionProvider : IApiDescriptionProvider
{
private readonly EndpointDataSource _endpointDataSource;
private readonly IHostEnvironment _environment;
private readonly IServiceProviderIsService? _serviceProviderIsService;
private readonly ParameterBindingMethodCache ParameterBindingMethodCache = new();

public EndpointMetadataApiDescriptionProvider(
EndpointDataSource endpointDataSource,
IHostEnvironment environment,
IServiceProviderIsService? serviceProviderIsService)
{
_endpointDataSource = endpointDataSource;
_environment = environment;
_serviceProviderIsService = serviceProviderIsService;
}

public void OnProvidersExecuting(ApiDescriptionProviderContext context)
{
//核心數(shù)據(jù)來自EndpointDataSource類
foreach (var endpoint in _endpointDataSource.Endpoints)
{
if (endpoint is RouteEndpoint routeEndpoint &&
routeEndpoint.Metadata.GetMetadata<MethodInfo>() is { } methodInfo &&
routeEndpoint.Metadata.GetMetadata<IHttpMethodMetadata>() is { } httpMethodMetadata &&
routeEndpoint.Metadata.GetMetadata<IExcludeFromDescriptionMetadata>() is null or { ExcludeFromDescription: false })
{
foreach (var httpMethod in httpMethodMetadata.HttpMethods)
{
context.Results.Add(CreateApiDescription(routeEndpoint, httpMethod, methodInfo));
}
}
}
}

private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string httpMethod, MethodInfo methodInfo)
{
//實現(xiàn)代碼省略
}
}

這個類里還有其他方法代碼也非常多,都是在組裝ApiDescription里的數(shù)據(jù),通過名稱可以得知,這個類是為了描述API接口信息用的,但是我們了解到的是它的數(shù)據(jù)源都來自EndpointDataSource類的實例。我們都知道MinimalApi提供的操作方法就是MapGetMapPostMapPutMapDelete等等,這些方法的本質(zhì)都是在調(diào)用Map方法,看一下核心實現(xiàn)

private static RouteHandlerBuilder Map(this IEndpointRouteBuilder endpoints,
RoutePattern pattern, Delegate handler, bool disableInferBodyFromParameters)
{
//省略部分代碼
var requestDelegateResult = RequestDelegateFactory.Create(handler, options);
var builder = new RouteEndpointBuilder(requestDelegateResult.RequestDelegate,pattern,defaultOrder)
{
//路由名稱
DisplayName = pattern.RawText ?? pattern.DebuggerToString(),
};

//獲得httpmethod
builder.Metadata.Add(handler.Method);

if (GeneratedNameParser.TryParseLocalFunctionName(handler.Method.Name, out var endpointName)
|| !TypeHelper.IsCompilerGeneratedMethod(handler.Method))
{
endpointName ??= handler.Method.Name;
builder.DisplayName = $"{builder.DisplayName} => {endpointName}";
}

var attributes = handler.Method.GetCustomAttributes();

foreach (var metadata in requestDelegateResult.EndpointMetadata)
{
builder.Metadata.Add(metadata);
}

if (attributes is not null)
{
foreach (var attribute in attributes)
{
builder.Metadata.Add(attribute);
}
}

// 添加ModelEndpointDataSource
var dataSource = endpoints.DataSources.OfType<ModelEndpointDataSource>().FirstOrDefault();
if (dataSource is null)
{
dataSource = new ModelEndpointDataSource();
endpoints.DataSources.Add(dataSource);
}
//將RouteEndpointBuilder添加到ModelEndpointDataSource
return new RouteHandlerBuilder(dataSource.AddEndpointBuilder(builder));
}

通過Map方法我們可以看到每次添加一個MinimalApi終結(jié)點都會給ModelEndpointDataSource實例添加一個EndpointBuilder實例,EndPointBuilder里承載著MinimalApi終結(jié)點的信息,而ModelEndpointDataSource則是繼承了EndpointDataSource類,這個可以看它的定義

internal class ModelEndpointDataSource : EndpointDataSource
{
}

這就和上面提到的EndpointMetadataApiDescriptionProvider里的EndpointDataSource聯(lián)系起來了,但是我們這里看到的是IEndpointRouteBuilderDataSources屬性,從名字看這明顯是一個集合,我們可以找到定義的地方看一下

public interface IEndpointRouteBuilder
{
IApplicationBuilder CreateApplicationBuilder();
IServiceProvider ServiceProvider { get; }
//這里是一個EndpointDataSource的集合
ICollection<EndpointDataSource> DataSources { get; }
}

這里既然是一個集合那如何和EndpointDataSource聯(lián)系起來呢,接下來我們就得去看EndpointDataSource是如何被注冊的即可,找到EndpointDataSource注冊的地方查看一下注冊代碼

var dataSources = new ObservableCollection<EndpointDataSource>();
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<RouteOptions>, ConfigureRouteOptions>(
serviceProvider => new ConfigureRouteOptions(dataSources)));

services.TryAddSingleton<EndpointDataSource>(s =>
{
return new CompositeEndpointDataSource(dataSources);
});

通過這段代碼我們可以得到兩點信息

我們可以簡單的來看下CompositeEndpointDataSource傳遞的dataSources是如何被接收的咱們只關(guān)注他說如何被接收的

public sealed class CompositeEndpointDataSource : EndpointDataSource
{
private readonly ICollection<EndpointDataSource> _dataSources = default!;
internal CompositeEndpointDataSource(ObservableCollection<EndpointDataSource> dataSources) : this()
{
_dataSources = dataSources;
}

public IEnumerable<EndpointDataSource> DataSources => _dataSources;
}

通過上面我們可以看到,系統(tǒng)默認為EndpointDataSource抽象類注冊了CompositeEndpointDataSource實現(xiàn)類,而這個實現(xiàn)類是一個組合類,它組合了一個EndpointDataSource的集合。那么到了這里就只剩下一個問題了,那就是EndpointDataSource是如何和IEndpointRouteBuilderDataSources屬性關(guān)聯(lián)起來的。現(xiàn)在有了提供數(shù)據(jù)源的IEndpointRouteBuilder,有承載數(shù)據(jù)的EndpointDataSource。這個地方呢大家也比較熟悉那就是UseEndpoints中間件里,我們來看下是如何實現(xiàn)的

public static IApplicationBuilder UseEndpoints(this IApplicationBuilder builder, Action<IEndpointRouteBuilder> configure)
{
// 省略一堆代碼

//得到IEndpointRouteBuilder實例
VerifyEndpointRoutingMiddlewareIsRegistered(builder, out var endpointRouteBuilder);
//獲取RouteOptions
var routeOptions = builder.ApplicationServices.GetRequiredService<IOptions<RouteOptions>>();
//遍歷IEndpointRouteBuilder的DataSources
foreach (var dataSource in endpointRouteBuilder.DataSources)
{
if (!routeOptions.Value.EndpointDataSources.Contains(dataSource))
{
//dataSource放入RouteOptions的EndpointDataSources集合
routeOptions.Value.EndpointDataSources.Add(dataSource);
}
}

return builder.UseMiddleware<EndpointMiddleware>();
}

private static void VerifyEndpointRoutingMiddlewareIsRegistered(IApplicationBuilder app, out IEndpointRouteBuilder endpointRouteBuilder)
{
if (!app.Properties.TryGetValue(EndpointRouteBuilder, out var obj))
{
throw new InvalidOperationException();
}

endpointRouteBuilder = (IEndpointRouteBuilder)obj!;

if (endpointRouteBuilder is DefaultEndpointRouteBuilder defaultRouteBuilder && !object.ReferenceEquals(app, defaultRouteBuilder.ApplicationBuilder))
{
throw new InvalidOperationException();
}
}

這里我們看到是獲取的IOptions<RouteOptions>里的EndpointDataSources,怎么和預(yù)想的劇本不一樣呢?并非如此,你看上面咱們說的這段代碼

var dataSources = new ObservableCollection<EndpointDataSource>();
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<RouteOptions>, ConfigureRouteOptions>(
serviceProvider => new ConfigureRouteOptions(dataSources)));

上面的dataSources同時傳遞給了CompositeEndpointDataSourceConfigureRouteOptions,而ConfigureRouteOptions則正是IConfigureOptions<RouteOptions>類型的,所以獲取IOptions<RouteOptions>就是獲取的ConfigureRouteOptions的實例,咱們來看一下ConfigureRouteOptions類的實現(xiàn)

internal class ConfigureRouteOptions : IConfigureOptions<RouteOptions>
{
private readonly ICollection<EndpointDataSource> _dataSources;

public ConfigureRouteOptions(ICollection<EndpointDataSource> dataSources)
{
if (dataSources == null)
{
throw new ArgumentNullException(nameof(dataSources));
}

_dataSources = dataSources;
}

public void Configure(RouteOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}

options.EndpointDataSources = _dataSources;
}
}

它的本質(zhì)操作就是對RouteOptions的EndpointDataSources的屬性進行操作,因為ICollection<EndpointDataSource>是引用類型,所以這個集合是共享的,因此IEndpointRouteBuilderDataSourcesIConfigureOptions<RouteOptions>本質(zhì)是使用了同一個ICollection<EndpointDataSource>集合,所以上面的UseEndpoints里獲取RouteOptions選項的本質(zhì)正是獲取的EndpointDataSource集合。

每次對IEndpointRouteBuilderDataSources集合Add的時候其實是在為ICollection<EndpointDataSource>集合添加數(shù)據(jù),而IConfigureOptions<RouteOptions>也使用了這個集合,所以它們的數(shù)據(jù)是互通的。 許多同學(xué)都很好強,默認并沒在MinimalApi看到注冊UseEndpoints,但是在ASP.NET Core6.0之前還是需要注冊UseEndpoints中間件的。這其實是ASP.NET Core6.0進行的一次升級優(yōu)化,因為很多操作默認都得添加,所以把它統(tǒng)一封裝起來了,這個可以在WebApplicationBuilder類中看到在ConfigureApplication方法中的代碼

private void ConfigureApplication(WebHostBuilderContext context, IApplicationBuilder app)
{
// 省略部分代碼

// 注冊UseDeveloperExceptionPage全局異常中間件
if (context.HostingEnvironment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.Properties.Add(WebApplication.GlobalEndpointRouteBuilderKey, _builtApplication);

if (_builtApplication.DataSources.Count > 0)
{
// 注冊UseRouting中間件
if (!_builtApplication.Properties.TryGetValue(EndpointRouteBuilderKey, out var localRouteBuilder))
{
app.UseRouting();
}
else
{
app.Properties[EndpointRouteBuilderKey] = localRouteBuilder;
}
}

app.Use(next =>
{
//調(diào)用WebApplication的Run方法
_builtApplication.Run(next);
return _builtApplication.BuildRequestDelegate();
});

// 如果DataSources集合有數(shù)據(jù)則注冊UseEndpoints
if (_builtApplication.DataSources.Count > 0)
{
app.UseEndpoints(_ => { });
}

// 省略部分代碼
}

相信大家通過ConfigureApplication這個方法大家就了解了吧,之前我們能看到的熟悉方法UseDeveloperExceptionPageUseRoutingUseEndpoints方法都在這里,畢竟之前這幾個方法幾乎也成了新建項目時候必須要添加的,所以微軟干脆就在內(nèi)部統(tǒng)一封裝起來了。

源碼小結(jié)

上面咱們分析了相關(guān)的源碼,整理起來就是這么一個思路。

這也給我們提供了一個思路,如果你想自己去適配swagger數(shù)據(jù)源的話完全也可以參考這個思路,想辦法把你要提供的接口信息放到EndpointDataSource的DataSources集合屬性里即可,或者直接適配IApiDescriptionGroupCollectionProvider里的數(shù)據(jù),有興趣的同學(xué)可以自行研究一下。

使用擴展

我們看到了微軟給我們提供了IApiDescriptionGroupCollectionProvider這個便利條件,所以如果以后有獲取接口信息的時候則可以直接使用了,很多時候比如寫監(jiān)控程序或者寫Api接口調(diào)用的代碼生成器的時候都可以考慮一下,咱們簡單的示例一下如何使用,首先定義個模型類來承載接口信息

public class ApiDoc
{
/// <summary>
/// 接口分組
/// </summary>
public string Group { get; set; }

/// <summary>
/// 接口路由
/// </summary>
public string Route { get; set; }

/// <summary>
/// http方法
/// </summary>
public string HttpMethod { get; set; }
}

這個類非常簡單只做演示使用,然后我們在IApiDescriptionGroupCollectionProvider里獲取信息來填充這個集合,這里我們寫一個htt接口來展示

app.MapGet("/apiinfo", (IApiDescriptionGroupCollectionProvider provider) => {
List<ApiDoc> docs = new List<ApiDoc>();
foreach (var group in provider.ApiDescriptionGroups.Items)
{
foreach (var apiDescription in group.Items)
{
docs.Add(new ApiDoc
{
Group = group.GroupName,
Route = apiDescription.RelativePath,
HttpMethod = apiDescription.HttpMethod
});
}
}
return docs;
});

這個時候當(dāng)你在瀏覽器里請求/apiinfo路徑的時候會返回你的webapi包含的接口相關(guān)的信息。咱們的示例是非常簡單的,實際上IApiDescriptionGroupCollectionProvider包含的接口信息是非常多的包含請求參數(shù)信息、輸出返回信息等很全面,這也是swagger可以完全依賴它的原因,有興趣的同學(xué)可以自行的了解一下,這里就不過多講解了。

總結(jié)

    本文咱們主要通過MinimalApi如何適配swagger的這么一個過程來講解了ASP.NET Core是如何給Swagger提供了數(shù)據(jù)的。本質(zhì)是微軟在ASP.NET Core本身提供了IApiDescriptionGroupCollectionProvider這么一個數(shù)據(jù)源,Swagger借助這個數(shù)據(jù)源生成了swagger文檔,IApiDescriptionGroupCollectionProvider來自聲明終結(jié)點的時候往EndpointDataSourceDataSources集合里添加的接口信息等。其實它內(nèi)部比這個還要復(fù)雜一點,不過如果我們用來獲取接口信息的話,大部分時候使用IApiDescriptionGroupCollectionProvider應(yīng)該就足夠了。

    分享一段我個人比較認可的話,與其天天鉆頭覓縫、找各種機會,不如把這些時間和金錢投入到自己的能力建設(shè)上。機會稍縱即逝,而且別人給你的機會,沒準(zhǔn)兒反而是陷阱。而投資個人能力就是積累一個資產(chǎn)賬戶,只能越存越多,看起來慢,但是你永遠在享受時間帶來的復(fù)利,其實快得很,收益也穩(wěn)定得多。有了能力之后,機會也就來了。

引用鏈接

[1] 點擊查看源碼??: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/v6.3.1/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs
[2] 點擊查看源碼??: https://github.com/dotnet/aspnetcore/blob/v6.0.5/src/Mvc/Mvc.ApiExplorer/src/DependencyInjection/EndpointMethodInfoApiExplorerServiceCollectionExtensions.cs#L20
[3] 點擊查看源碼??: https://github.com/dotnet/aspnetcore/blob/v6.0.5/src/Mvc/Mvc.ApiExplorer/src/ApiDescriptionGroupCollectionProvider.cs
[4] 點擊查看源碼??: https://github.com/dotnet/aspnetcore/blob/v6.0.5/src/Mvc/Mvc.ApiExplorer/src/ApiDescriptionGroupCollectionProvider.cs
[5] 點擊查看源碼??: https://github.com/dotnet/aspnetcore/blob/v6.0.5/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs#L419
[6] 點擊查看源碼??: https://github.com/dotnet/aspnetcore/blob/v6.0.5/src/Http/Routing/src/ModelEndpointDataSource.cs
[7] 點擊查看源碼??: https://github.com/dotnet/aspnetcore/blob/v6.0.5/src/Http/Routing/src/IEndpointRouteBuilder.cs
[8] 點擊查看源碼??: https://github.com/dotnet/aspnetcore/blob/v6.0.5/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs#L57
[9] 點擊查看源碼??: https://github.com/dotnet/aspnetcore/blob/v6.0.5/src/Http/Routing/src/CompositeEndpointDataSource.cs#L22:54
[10] 點擊查看源碼??: https://github.com/dotnet/aspnetcore/blob/19d21ad0d209b5c7be6387c7db3cf202c91951af/src/Http/Routing/src/Builder/EndpointRoutingApplicationBuilderExtensions.cs#L99
[11] 點擊查看源碼??: https://github.com/dotnet/aspnetcore/blob/v6.0.5/src/Http/Routing/src/ConfigureRouteOptions.cs
[12] 點擊查看源碼??: https://github.com/dotnet/aspnetcore/blob/v6.0.5/src/DefaultBuilder/src/WebApplicationBuilder.cs#L245

本文章轉(zhuǎn)載微信公眾號@DotNET技術(shù)圈

上一篇:

開源機器學(xué)習(xí)框架:Scikit-learn API簡介

下一篇:

微博熱搜API | 高性能熱搜API及基于Ak、Sk鑒權(quán)方案設(shè)計思路與實現(xiàn)
#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊

多API并行試用

數(shù)據(jù)驅(qū)動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費