本文共 5433 字,大约阅读时间需要 18 分钟。
目录
该博客条目通过向我现有的Web应用程序中添加第二个swagger文件,并控制其中的内容来进行。
最近,除了我的SPA(Angular)应用使用的现有API之外,一位客户要求我建立面向Web的小型最终用户。
这似乎是一个很好的机会,可以写博客介绍我的经验,并与更多的读者分享我的方法和解决方案的知识。
我当前的应用程序是基于带有Angular模板的构建的。虽然这对这个故事来说并不重要,但重要的是,它是一个ASP.NET Core应用程序,(“生成漂亮的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文件在:公开,第二个在公开。
那是一个开始。如果您喜欢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的工作原理。事实证明,它在内部使用,这是ASP.NET Core附带的API元数据层。特别是,它使用该ApiDescription.GroupName属性来确定将哪些方法放入哪些文件中。如果该属性是null或它和文档名称(例如“client-v1”)相等,则Swashbuckle将其包括在内。并且,默认设置是null,这就是两个Swagger文件都相同的原因。
有两种方法设置GroupName。我可以通过在控制器的每个方法上设置属性来进行设置,但这将是乏味且难以维护的。相反,我选择了神奇的路由。
这涉及注册动作约定,并根据命名空间将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获胜。
万一您错过了它,我是。这是一个依赖管理工具(例如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 TaskGetProduct(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 TaskGetProduct(int id)
这产生了await clientApiProxy.GetProductAsync(productId);我所期望的。
这篇文章是关于如何生成未经身份验证的客户端的故事。
同时,所有代码都可以在multi 分支中运行,也可以在演示站点的 Pull Request中。我希望这是有帮助的。
转载地址:http://gbwhj.baihongyu.com/