当前位置: 代码网 > it编程>编程语言>Asp > 源码分析MinimalApi是如何在Swagger中展示

源码分析MinimalApi是如何在Swagger中展示

2024年05月15日 Asp 我要评论
前言之前看到技术群里有同学讨论说对于minimalapi能接入到swagger中感到很神奇,加上swagger的数据本身是支持openapi2.0和openapi3.0使得swagger.json成为

前言

之前看到技术群里有同学讨论说对于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
{
    /// &lt;summary&gt;
    /// 接口分组
    /// &lt;/summary&gt;
    public string group { get; set; }
    /// &lt;summary&gt;
    /// 接口路由
    /// &lt;/summary&gt;
    public string route { get; set; }
    /// &lt;summary&gt;
    /// http方法
    /// &lt;/summary&gt;
    public string httpmethod { get; set; }
}

这个类非常简单只做演示使用,然后我们在iapidescriptiongroupcollectionprovider里获取信息来填充这个集合,这里我们写一个htt接口来展示

app.mapget("/apiinfo", (iapidescriptiongroupcollectionprovider provider) =&gt; {
    list&lt;apidoc&gt; docs = new list&lt;apidoc&gt;();
    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应该就足够了。    

分享一段我个人比较认可的话,与其天天钻头觅缝、找各种机会,不如把这些时间和金钱投入到自己的能力建设上。机会稍纵即逝,而且别人给你的机会,没准儿反而是陷阱。而投资个人能力就是积累一个资产账户,只能越存越多,看起来慢,但是你永远在享受时间带来的复利,其实快得很,收益也稳定得多。有了能力之后,机会也就来了。

以上就是源码分析minimalapi是如何在swagger中展示的详细内容,更多关于minimalapi在swagger展示的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com