目录
更多Cowbell Swagger
探索ApiExplorer
消费Swagger
...没那么快
最后的技巧和窍门
结论
该博客条目通过向我现有的Web应用程序中添加第二个swagger文件,并控制其中的内容来进行。
最近,除了我的SPA(Angular)应用使用的现有API之外,一位客户要求我建立面向Web的小型最终用户。
这似乎是一个很好的机会,可以写博客介绍我的经验,并与更多的读者分享我的方法和解决方案的知识。
我当前的应用程序是基于带有Angular模板的ASP.NET Boilerplate构建的。虽然这对这个故事来说并不重要,但重要的是,它是一个ASP.NET Core应用程序,Swashbuckle(“生成漂亮的API文档”的工具)将在其中生成Swagger文档。
最初,我曾考虑向部署了我的站点的Kubernetes集群添加一个额外的微服务。问题是新API很小,并且涉及设置安全性、DI、日志记录、应用程序设置、配置、docker和Kubernetes端口路由所涉及的工作量似乎太多了。
我想要更轻巧的替代方案,以扩展现有的安全模型并保留现有的配置。像这样:
向我现有的Web应用程序中添加第二个swagger文件相对容易。控制其中的内容,要少一些。
要添加第二个swagger文件,我只需要在Startup.cs中的services.AddSwaggerGen中第二次调用.SwaggerDoc即可。
services.AddSwaggerGen(options =>
{
// add two swagger files, one for the web app and one for clients
options.SwaggerDoc("v1", new OpenApiInfo()
{
Title = "LeesStore API",
Version = "v1"
});
options.SwaggerDoc("client-v1", new OpenApiInfo
{
Title = "LeesStore Client API",
Version = "client-v1"
});
从技术上讲,这是说我有两个版本的同一API,而不是两个单独的API,但是效果是相同的。第一个swagger文件在:http://localhost/swagger/v1/swagger.json公开,第二个在http://localhost/swagger/client-v1/swagger.json公开。
那是一个开始。如果您喜欢Swashbuckle所提供的Swagger UI(与我一样),您将同意尝试将两个swagger文件添加到其中。在Startup.cs中的UseSwaggerUI调用中第二次调用.SwaggerEndpoint,事实证明这很容易:
app.UseSwaggerUI(options =>
{
var baseUrl = _appConfiguration["App:ServerRootAddress"]
.EnsureEndsWith('/');
options.SwaggerEndpoint(
$"{baseUrl}swagger/v1/swagger.json",
"LeesStore API V1");
options.SwaggerEndpoint(
$"{baseUrl}swagger/client-v1/swagger.json",
"LeesStore Client API V1");
现在,我可以在右上方的“选择定义”下拉列表中的两个swagger文件之间进行选择:
很好,对吗?
例外:两个页面看起来相同。这是因为所有方法当前都包含在两个定义中。
为了解决这个问题,我需要深入研究Swashbuckle的工作原理。事实证明,它在内部使用ApiExplorer,这是ASP.NET Core附带的API元数据层。特别是,它使用该ApiDescription.GroupName属性来确定将哪些方法放入哪些文件中。如果该属性是null或它和文档名称(例如“client-v1”)相等,则Swashbuckle将其包括在内。并且,默认设置是null,这就是两个Swagger文件都相同的原因。
有两种方法设置GroupName。我可以通过在控制器的每个方法上设置ApiExplorerSettings属性来进行设置,但这将是乏味且难以维护的。相反,我选择了神奇的路由。
这涉及注册动作约定,并根据命名空间将action分配给文档,如下所示:
public class SwaggerFileMapperConvention : IControllerModelConvention
{
public void Apply(ControllerModel controller)
{
var controllerNamespace = controller?.ControllerType?.Namespace;
if (controllerNamespace == null) return;
var namespaceElements = controllerNamespace.Split('.');
var nextToLastNamespace = namespaceElements.ElementAtOrDefault
(namespaceElements.Length - 2)?.ToLowerInvariant();
var isInClientNamespace = nextToLastNamespace == "client";
controller.ApiExplorer.GroupName = isInClientNamespace ? "client-v1" : "v1";
}
}
如果运行该命令,则会看到所有内容仍然重复。这是因为Startup.cs中这行:
services.AddSwaggerGen(options =>{
options.DocInclusionPredicate((docName, description) => true);
当发生冲突时,DocInclusionPredicate获胜。
万一您错过了它,我是Cake的忠实粉丝。这是一个依赖管理工具(例如Make,Rake,Maven,Grunt或Gulp),可以使用C#编写脚本。它包含一个NSwag插件,它是从swagger文件自动生成代理的几种工具之一。因此,我生成了这样的代理:
#addin nuget:?package=Cake.CodeGen.NSwag&version=1.2.0&loaddependencies=true
…
Task("CreateProxy")
.Description("Uses nswag to re-generate a c# proxy to the client api.")
.Does(() =>
{
var filePath = DownloadFile("http://localhost:21021/swagger/client-v1/swagger.json");
Information("client swagger file downloaded to: " + filePath);
var proxyClass = "ClientApiProxy";
var proxyNamespace = "LeesStore.Cmd.ClientProxy";
var destinationFile = File("./aspnet-core/src/LeesStore.Cmd/ClientProxy/ClientApiProxy.cs");
var settings = new CSharpClientGeneratorSettings
{
ClassName = proxyClass,
CSharpGeneratorSettings =
{
Namespace = proxyNamespace
}
};
NSwag.FromJsonSpecification(filePath)
.GenerateCSharpClient(destinationFile, settings);
});
在Mac/linux 上运行build.ps1 -target CreateProxy或build.sh -target CreateProxy,然后弹出一个我可以在这样的控制台中使用的强类型ClientApiProxy类:
using var httpClient = new HttpClient();
var clientApiProxy = new ClientApiProxy("http://localhost:21021/", httpClient);
var product = await clientApiProxy.ProductAsync(productId);
Console.WriteLine($"Your product is: '{product.Name}'");
大团圆结局,每个人都赢吧?不完全的。如果您在ASP.NET Boilerplate中运行,则始终返回Your product is ""。为什么?安静的失败很难追踪。看着Fiddler中的网站流量,我看到了以下内容:
{"result":{"name":"The Product","quantity":0,"id":2},
"targetUrl":null,"success":true,"error":null,"unAuthorizedRequest":false,"__abp":true}
乍看之下,这似乎是合理的。但是,这不会反序列化为ProductDto,因为JSON中的ProductDto处于“result”对象内。包装功能是ABP如何(以及其他方式)在漂亮的模态对话框中将UserFriendlyException消息返回给用户。
上面的屏幕截图来自JSON,如下所示:
{"result":null,"targetUrl":null,"success":false,
"error":{"code":0,"message":"Dude, an exception just occurred,
maybe you should check on that","details":null,"validationErrors":null},
"unAuthorizedRequest":false,"__abp":true}
事实证明该解决方案非常简单。将DontWrapResult属性放到控制器上:
[DontWrapResult(WrapOnError = false, WrapOnSuccess = false, LogError = true)]
public class ProductController : LeesStoreControllerBase
结果是干净的JSON:
{"name":"The Product","quantity":0,"id":2}
和控制台应用程序一起编写Your product is "The Product"。
太棒了!
最后一件事。该方法名称“ProductAsync”似乎有点不幸。它从哪里来?
原来是我写的:
[HttpGet("api/client/v1/product/{id}")]
public async Task GetProduct(int id)
该ApiExplorer只露出了终点,而不是方法名。因此,Swashbuckle 在Swagger文件中不包含operationId,NSwag被迫使用端点中的元素来命名。
解决方法是指定名称,以便Swashbuckle可以生成一个operationId。使用or 属性中的属性很容易。使用HttpGet或HttpPost属性中的Name属性很容易做到这一点。多亏了C#6中的nameof,我们可以使它保持强类型。
[HttpGet("api/client/v1/product/{id}", Name = nameof(GetProduct))]
public async Task GetProduct(int id)
这产生了await clientApiProxy.GetProductAsync(productId);我所期望的。
这篇文章是关于如何生成未经身份验证的客户端的故事。
同时,所有代码都可以在multi -api分支中运行,也可以在LeesStore演示站点的Multiple API的 Pull Request中仔细阅读。我希望这是有帮助的。