深入探究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類中[點擊查看源碼👈]只摘要我們關注的地方即可

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

{
  "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倉庫裏找到方法位置[點擊查看源碼👈]看一下方法實現

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接口的默認實現ApiDescriptionGroupCollectionProvider類裏的實現[點擊查看源碼👈]

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

		//這裏使用了_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默認實現是EndpointMetadataApiDescriptionProvider類[點擊查看源碼👈]看一下相實現

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方法[點擊查看源碼👈],看一下核心實現

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終結點都會給ModelEndpointDataSource實例添加一個EndpointBuilder實例,EndPointBuilder裏承載着MinimalApi終結點的信息,而ModelEndpointDataSource則是繼承了EndpointDataSource類,這個可以看它的定義[點擊查看源碼👈]

internal class ModelEndpointDataSource : EndpointDataSource
{
}

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

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

這裏既然是一個集合那如何和EndpointDataSource聯繫起來呢,接下來我們就得去看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);
});

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

  • 一是EndpointDataSource這個抽象類,系統給他註冊的是CompositeEndpointDataSource這個子類,看名字可以看出是組合的EndpointDataSource
  • 二是CompositeEndpointDataSource是通過ObservableCollection<EndpointDataSource>這麼一個集合來初始化的

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

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中間件裏,我們來看下是如何實現的[點擊查看源碼👈]

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,怎麼和預想的劇本不一樣呢?並非如此,你看上面咱們說的這段代碼

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

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類中看到[點擊查看源碼👈]在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集合有數據則註冊UseEndpoints
    if (_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應該就足夠了。
    分享一段我個人比較認可的話,與其天天鑽頭覓縫、找各種機會,不如把這些時間和金錢投入到自己的能力建設上。機會稍縱即逝,而且別人給你的機會,沒準兒反而是陷阱。而投資個人能力就是積累一個資產賬戶,只能越存越多,看起來慢,但是你永遠在享受時間帶來的複利,其實快得很,收益也穩定得多。有了能力之後,機會也就來了。

👇歡迎掃碼關注我的公衆號👇
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章