前言
之前看到技术群里有同学讨论说对于minimalapi能接入到swagger中感到很神奇,加上swagger的数据本身是支持openapi2.0和openapi3.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提供的操作方法就是mapget、mappost、mapput、mapdelete等等,这些方法的本质都是在调用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联系起来了,但是我们这里看到的是iendpointroutebuilder的datasources属性,从名字看这明显是一个集合,我们可以找到定义的地方看一下[点击查看源码👈]
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是如何和iendpointroutebuilder的datasources属性关联起来的。现在有了提供数据源的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同时传递给了compositeendpointdatasource和configurerouteoptions,而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>是引用类型,所以这个集合是共享的,因此iendpointroutebuilder的datasources和iconfigureoptions<routeoptions>本质是使用了同一个icollection<endpointdatasource>集合,所以上面的useendpoints里获取routeoptions选项的本质正是获取的endpointdatasource集合。
每次对iendpointroutebuilder的datasources集合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这个方法大家就了解了吧,之前我们能看到的熟悉方法usedeveloperexceptionpage、userouting、useendpoints方法都在这里,毕竟之前这几个方法几乎也成了新建项目时候必须要添加的,所以微软干脆就在内部统一封装起来了。
源码小结
上面咱们分析了相关的源码,整理起来就是这么一个思路。
swashbuckle.aspnetcore.swaggergen用来生成swagger的数据源来自iapidescriptiongroupcollectionprovider- iapidescriptiongroupcollectionprovider实例的数据来自
endpointdatasource - 因为
endpointdatasource的datasources和iconfigureoptions<routeoptions>本质是使用了同一个icollection<endpointdatasource>集合,所以它们是同一份数据 - 每次使用minimalapi的map相关的方法的是会给
iendpointroutebuilder的datasources集合添加数据 - 在
useendpoints中间件里获取iendpointroutebuilder的datasources数据给routeoptions选项的endpointdatasources集合属性添加数据,本质则是给icollection<endpointdatasource>集合赋值,自然也就是给endpointdatasource的datasources属性赋值
这也给我们提供了一个思路,如果你想自己去适配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来自声明终结点的时候往endpointdatasource的datasources集合里添加的接口信息等。其实它内部比这个还要复杂一点,不过如果我们用来获取接口信息的话,大部分时候使用iapidescriptiongroupcollectionprovider应该就足够了。
分享一段我个人比较认可的话,与其天天钻头觅缝、找各种机会,不如把这些时间和金钱投入到自己的能力建设上。机会稍纵即逝,而且别人给你的机会,没准儿反而是陷阱。而投资个人能力就是积累一个资产账户,只能越存越多,看起来慢,但是你永远在享受时间带来的复利,其实快得很,收益也稳定得多。有了能力之后,机会也就来了。
以上就是源码分析minimalapi是如何在swagger中展示的详细内容,更多关于minimalapi在swagger展示的资料请关注代码网其它相关文章!
发表评论