深入探究MinimalApi是如何在Swagger中展示的

前言

????之前看到技術群里有同學討論說對于MinimalApi能接入到Swagger中感到很神奇,加上Swagger的數據本身是支持OpenApi2.0OpenApi3.0使得swagger.json成為了許多接口文檔管理工具的標準數據源。ASP.NET Core能夠輕松快速的集成Swagger得益于微軟對OpenApi的大力支持,大部分情況下幾乎是添加默認配置,就能很好的工作了。這一切都是得益于ASP.NET Core底層提供了對接口元數據的描述和對終結點的相關描述。本文我們就通過MinimalApi來了解一下ASP.NET Core為何能更好的集成Swagger。

使用方式

雖然我們討論的是MInimalApi與Swagger數據源的關系,但是為了使得看起來更清晰,我們還是先看一下MinimalApi如何集成到Swagger,直接上代碼

var?builder?=?WebApplication.CreateBuilder(args);//這是重點,是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終結點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中展示就必須要添加這個服務。所以Swagger還是那個Swagger,變的是ASP.NET Core本身,但是變化是如何適配數據源的問題,Swagger便是建立在這個便利基礎上。接下來咱們就通過源碼看一下它們之間的關系。

源碼探究

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

swagger的數據源

熟悉Swashbuckle.AspNetCore的應該知道它其實是由幾個程序集一起構建的,也就是說Swashbuckle.AspNetCore本身是一個解決方案,不過這不是重點,其中生成Swagger.json的是在Swashbuckle.AspNetCore.SwaggerGen程序集中,直接找到位置在SwaggerGenerator類中[點擊查看源碼👈[1]]只摘要我們關注的地方即可

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核心數據源源來自_apiDescriptionsProvidervar?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組裝是來自applicableApiDescriptionsPaths?=?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這個類的結構一定是一目了然,不信的話你可以自行看看它的結構

{"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。所以小結一下,Swagger接口的文檔信息的數據源來自于IApiDescriptionGroupCollectionProvider

ASP.Net Core如何提供

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

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

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

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方法返回的數據_apiDescriptionGroups?=?GetCollection(actionDescriptors);}return?_apiDescriptionGroups;}}private?ApiDescriptionGroupCollection?GetCollection(ActionDescriptorCollection?actionDescriptors){var?context?=?new?ApiDescriptionProviderContext(actionDescriptors.Items);//這里使用了_apiDescriptionProvidersforeach?(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默認實現是EndpointMetadataApiDescriptionProvider類[點擊查看源碼👈[4]]看一下相實現

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){//核心數據來自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){//實現代碼省略?}
}

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

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(),};//獲得httpmethodbuilder.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);}}//?添加ModelEndpointDataSourcevar?dataSource?=?endpoints.DataSources.OfType<ModelEndpointDataSource>().FirstOrDefault();if?(dataSource?is?null){dataSource?=?new?ModelEndpointDataSource();endpoints.DataSources.Add(dataSource);}//將RouteEndpointBuilder添加到ModelEndpointDataSourcereturn?new?RouteHandlerBuilder(dataSource.AddEndpointBuilder(builder));
}

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

internal?class?ModelEndpointDataSource?:?EndpointDataSource
{
}

這就和上面提到的EndpointMetadataApiDescriptionProvider里的EndpointDataSource聯系起來了,但是我們這里看到的是IEndpointRouteBuilderDataSources屬性,從名字看這明顯是一個集合,我們可以找到定義的地方看一下[點擊查看源碼👈[7]]

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

這里既然是一個集合那如何和EndpointDataSource聯系起來呢,接下來我們就得去看EndpointDataSource是如何被注冊的即可,找到EndpointDataSource注冊的地方[點擊查看源碼👈[8]]查看一下注冊代碼

var?dataSources?=?new?ObservableCollection<EndpointDataSource>();
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<RouteOptions>,?ConfigureRouteOptions>(serviceProvider?=>?new?ConfigureRouteOptions(dataSources)));services.TryAddSingleton<EndpointDataSource>(s?=>
{return?new?CompositeEndpointDataSource(dataSources);
});

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

  • ??一是EndpointDataSource這個抽象類,系統給他注冊的是CompositeEndpointDataSource這個子類,看名字可以看出是組合的EndpointDataSource

  • ??二是CompositeEndpointDataSource是通過ObservableCollection<EndpointDataSource>這么一個集合來初始化的

我們可以簡單的來看下CompositeEndpointDataSource傳遞的dataSources是如何被接收的[點擊查看源碼👈[9]]咱們只關注他說如何被接收的

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

通過上面我們可以看到,系統默認為EndpointDataSource抽象類注冊了CompositeEndpointDataSource實現類,而這個實現類是一個組合類,它組合了一個EndpointDataSource的集合。那么到了這里就只剩下一個問題了,那就是EndpointDataSource是如何和IEndpointRouteBuilderDataSources屬性關聯起來的。現在有了提供數據源的IEndpointRouteBuilder,有承載數據的EndpointDataSource。這個地方呢大家也比較熟悉那就是UseEndpoints中間件里,我們來看下是如何實現的[點擊查看源碼👈[10]]

public?static?IApplicationBuilder?UseEndpoints(this?IApplicationBuilder?builder,?Action<IEndpointRouteBuilder>?configure)
{//?省略一堆代碼//得到IEndpointRouteBuilder實例VerifyEndpointRoutingMiddlewareIsRegistered(builder,?out?var?endpointRouteBuilder);//獲取RouteOptionsvar?routeOptions?=?builder.ApplicationServices.GetRequiredService<IOptions<RouteOptions>>();//遍歷IEndpointRouteBuilder的DataSourcesforeach?(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,怎么和預想的劇本不一樣呢?并非如此,你看上面咱們說的這段代碼

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類的實現[點擊查看源碼👈[11]]

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;}
}

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

每次對IEndpointRouteBuilderDataSources集合Add的時候其實是在為ICollection<EndpointDataSource>集合添加數據,而IConfigureOptions<RouteOptions>也使用了這個集合,所以它們的數據是互通的。 許多同學都很好強,默認并沒在MinimalApi看到注冊UseEndpoints,但是在ASP.NET Core6.0之前還是需要注冊UseEndpoints中間件的。這其實是ASP.NET Core6.0進行的一次升級優化,因為很多操作默認都得添加,所以把它統一封裝起來了,這個可以在WebApplicationBuilder類中看到[點擊查看源碼👈[12]]在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?=>{//調用WebApplication的Run方法_builtApplication.Run(next);return?_builtApplication.BuildRequestDelegate();});//?如果DataSources集合有數據則注冊UseEndpointsif?(_builtApplication.DataSources.Count?>?0){app.UseEndpoints(_?=>?{?});}//?省略部分代碼
}

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

源碼小結

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

  • ??Swashbuckle.AspNetCore.SwaggerGen用來生成swagger的數據源來自IApiDescriptionGroupCollectionProvider

  • ??IApiDescriptionGroupCollectionProvider實例的數據來自EndpointDataSource

  • ??因為EndpointDataSourceDataSourcesIConfigureOptions<RouteOptions>本質是使用了同一個ICollection<EndpointDataSource>集合,所以它們是同一份數據

  • ??每次使用MinimalApi的Map相關的方法的是會給IEndpointRouteBuilderDataSources集合添加數據

  • ??在UseEndpoints中間件里獲取IEndpointRouteBuilderDataSources數據給RouteOptions選項的EndpointDataSources集合屬性添加數據,本質則是給ICollection<EndpointDataSource>集合賦值,自然也就是給EndpointDataSourceDataSources屬性賦值

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

使用擴展

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

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;
});

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

總結

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

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

引用鏈接

[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

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/286423.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/286423.shtml
英文地址,請注明出處:http://en.pswp.cn/news/286423.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

語句的輸入、輸出

一、C#基礎 1、項目后綴&#xff1a; .config——配置文件&#xff08;存放配置參數文件&#xff09; .csproj——項目文件&#xff08;管理文件項&#xff09; .sln——解決方案文件&#xff08;管理項目&#xff09; .cs——源文件&#xff08;程序代碼&#xff09; 2、函數四…

Samba平臺搭建和用戶自行修改密碼環境搭建筆記

Samba 平臺搭建和用戶自行修改密碼環境搭建筆記系統&#xff1a;CentOS release 6.5 (Final)x86_64軟件: samba #服務端samba-client #客戶端samba-common #通用工具和庫Apache:httpdWeb: changepassword-0.9.tar.gz #需 GCC 編譯源碼包&#…

Android使用C/C++來保存密鑰

Android使用C/C來保存密鑰本文主要介紹如何通過native方法調用取出密鑰&#xff0c;以替代原本直接寫在Java中&#xff0c;或寫在gradle腳本中的不安全方式。為什么要這么做 如果需要在本地存儲一個密鑰串&#xff0c;典型的方式有 1. 直接寫在java source code中 2. 寫在gradl…

無代碼iVX編程實現簡單跳躍超級瑪麗游戲

首先咱們打開 iVX 的在線編輯器&#xff1a;https://editor.ivx.cn/ 隨后咱們選擇2D游戲類型制作一個簡單跳躍游戲&#xff1a; 接下來創建幾個圖片&#xff0c;并且添加物體&#xff0c;如圖所示&#xff1a; 在此需要更改對應稱重地面的阻尼值&#xff0c;讓其能夠緩慢降落…

【三維激光掃描】實驗01:環境搭建CAD2014+StonexSiScan軟件安裝

目 錄 一、CAD2014簡體中文版安裝1. 安裝過程2. 激活過程二、Si-Scan安裝1. 主程序安裝2. 驅動安裝一、CAD2014簡體中文版安裝 1. 安裝過程 雙擊安裝包:AutoCAD_2014_Simplified_Chinese_Win_64bit_dlm.sfx.exe,進行自解壓。 解壓完成后,如下圖所示,點擊【安裝】。 接受許…

C# 11 新特性:原始字符串

之前我們經常需要使用 string 類型定義字符串文本&#xff0c;字符串文本用一對雙引號括起來表示&#xff1a;var str "Hello MyIO";字符串可包含任何字符文本&#xff0c;但是有些字符需要轉義才能表示&#xff0c;比如雙引號要轉義成\"&#xff1a;var str …

File,FileInfo,Directory,DirectoryInfo

兩者的共同點&#xff1a; 一&#xff1a;都用于典型的操作&#xff0c;如復制、移動、重命名、創建、打開、刪除和追加到文件 二&#xff1a;默認情況下&#xff0c;將向所有用戶授予對新文件的完全讀/寫訪問權限。 兩者的區別&#xff1a; File類是靜態類&#xff0c;由…

C語言試題四之計算并輸出3到n之間所有素數的平方根之和

??個人主頁:個人主頁 ??系列專欄:C語言試題200例目錄 ??推薦一款刷算法、筆試、面經、拿大公司offer神器 ?? 點擊跳轉進入網站 ?作者簡介:大家好,我是碼莎拉蒂,CSDN博客專家(全站排名Top 50),阿里云博客專家、51CTO博客專家、華為云享專家 1、題目 請編寫函數…

bzoj1011

因為允許5%的誤差。。所以把&#xff1e;一定長度的一段看成一段近似計算就行了。。 1 #include<cstdio>2 #include<cstdlib>3 #include<cstring>4 #include<ctime>5 #include<cmath>6 #include<iostream>7 #include<algorithm>8 #i…

一名全棧工程師的必備“百寶箱”

摘要&#xff1a;全棧工程師&#xff0c;也叫全端工程師&#xff0c;是指掌握多種技能&#xff0c;并能利用多種技能獨立完成產品的人。全棧工程師熟悉多種開發語言&#xff0c;同時具備前端和后臺開發能力&#xff0c;從需求分析&#xff0c;原型設計到產品開發&#xff0c;測…

為VMware虛擬主機添加新磁盤

軌跡: 關閉VMware虛擬主機 ---> 虛擬機 ---> 設置 ---> 硬件 ---> 硬盤 ---> 添加 ---> (彈出添加硬件向導)硬盤 ---> 磁盤類型 ---> 選擇磁盤 ---> 指定磁盤容量(最好選擇“將虛擬磁盤存儲為單個文件”) ---> 指定磁盤文件 ---> 點擊“完成…

【ArcGIS風暴】全站儀、RTK測量坐標數據在CASS和ArcGIS中展點的區別和聯系(帶數據)

ArcGIS展經緯度點完整教程:【ArcGIS風暴】ArcGIS 10.2導入Excel數據X、Y坐標(經緯度、平面坐標),生成Shapefile點數據圖層 目錄 1. CASS展點操作步驟2. ArcGIS展點操作步驟3. 案例數據下載RTK或全站儀地面實測的三維坐標數據文件一般包括點號,編碼,東坐標,北坐標,高程等…

php一篇文零基礎到制作在線圖片編輯網站賺錢(gif壓縮、九宮格裁剪、等比裁剪、大小變換)【php華為云實戰】

注意本篇文適用于&#xff1a; 零基礎小白想要了解一下php開發或者網站開發的同學&#xff08;但是注意&#xff0c;零基礎你可以通過本篇完成&#xff0c;但是由于是速成會有一些難度&#xff0c;本篇內容由于是速成&#xff0c;有一些額外知識點&#xff0c;不會可以來問我1…

Mustache.js使用筆記(內容屬于轉載總結)

1、Mustache的語法很簡單&#xff0c;用兩個大括號標記要綁定的字段即可&#xff0c;“{{}}” Mustache主要的渲染方法為Mustache.render()&#xff0c;該方法有兩個參數&#xff0c;第一個為要渲染的模版&#xff0c; 也就是例子中的template&#xff0c;第二個就是數據源也就…

MAUI 自定義繪圖入門

在2022的5月份&#xff0c;某軟正式發布了 MAUI 跨平臺 UI 框架。我本來想著趁六一兒童節放假來寫幾篇關于 MAUI 入門的博客&#xff0c;可惜發現我不擅長寫很入門的博客。再加上 MAUI 似乎是為了趕發布日期而發布&#xff0c;只能勉強說能開發了&#xff0c;能用了。于是我就來…

C語言試題五之計算并輸出給定數組(長度為9)中每相鄰兩個元素之平均值的平方根之和

??個人主頁:個人主頁 ??系列專欄:C語言試題200例目錄 ??推薦一款刷算法、筆試、面經、拿大公司offer神器 ?? 點擊跳轉進入網站 ?作者簡介:大家好,我是碼莎拉蒂,CSDN博客專家(全站排名Top 50),阿里云博客專家、51CTO博客專家、華為云享專家 1、題目 請編寫函數…

【三維激光掃描】實驗02:StonexSiScan新建項目、加載點云數據

文章目錄 1. 新建工程2. 打開工程3. 加載點云1. 新建工程 打開StonexSiScan點云后處理軟件,點擊【新建】按鈕。 選擇工程存放路徑,輸入工程名稱。 2. 打開工程 點擊【打開】按鈕。

eBPF 在云原生環境中的應用

端午假期&#xff0c;我翻譯了 OReilly 的報告《什么是 eBPF》&#xff0c;其中我覺得第五章「云原生環境中的 eBPF」解答了我心中的很多疑惑&#xff0c;比較不錯&#xff0c;分享給大家。下面是第五章譯文。《什么是 eBPF》中文版封面近年來&#xff0c;云原生應用已呈指數級…

使用HtmlAgilityPack抓取網頁數據

XPath路徑表達式&#xff0c;主要是對XML文檔中的節點進行搜索&#xff0c;通過XPath表達式可以對XML文檔中的節點位置進行快速定位和訪問&#xff0c;html也是也是一種類似于xml的標記語言&#xff0c;但是語法沒有那么嚴謹&#xff0c;在codeplex里有一個開源項目HtmlAgility…