深入探究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.mzph.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…