700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > 使用Swagger ApiExplorer和NSwag掌握ASP.NET Core和ABP中的外部Web API

使用Swagger ApiExplorer和NSwag掌握ASP.NET Core和ABP中的外部Web API

时间:2020-08-28 08:58:59

相关推荐

使用Swagger ApiExplorer和NSwag掌握ASP.NET Core和ABP中的外部Web API

目录

更多CowbellSwagger

探索ApiExplorer

消费Swagger

...没那么快

最后的技巧和窍门

结论

该博客条目通过向我现有的Web应用程序中添加第二个swagger文件,并控制其中的内容来进行。

最近,除了我的SPA(Angular)应用使用的现有API之外,一位客户要求我建立面向Web的小型最终用户。

这似乎是一个很好的机会,可以写博客介绍我的经验,并与更多的读者分享我的方法和解决方案的知识。

我当前的应用程序是基于带有Angular模板的 Boilerplate构建的。虽然这对这个故事来说并不重要,但重要的是,它是一个 Core应用程序,Swashbuckle(“生成漂亮的API文档”的工具)将在其中生成Swagger文档。

最初,我曾考虑向部署了我的站点的Kubernetes集群添加一个额外的微服务。问题是新API很小,并且涉及设置安全性、DI、日志记录、应用程序设置、配置、docker和Kubernetes端口路由所涉及的工作量似乎太多了。

我想要更轻巧的替代方案,以扩展现有的安全模型并保留现有的配置。像这样:

更多CowbellSwagger

向我现有的Web应用程序中添加第二个swagger文件相对容易。控制其中的内容,要少一些。

要添加第二个swagger文件,我只需要在Startup.cs中的services.AddSwaggerGen中第二次调用.SwaggerDoc即可。

services.AddSwaggerGen(options =>{// add two swagger files, one for the web app and one for clientsoptions.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文件之间进行选择

很好,对吗?

例外:两个页面看起来相同。这是因为所有方法当前都包含在两个定义中。

探索ApiExplorer

为了解决这个问题,我需要深入研究Swashbuckle的工作原理。事实证明,它在内部使用ApiExplorer,这是 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获胜。

消费Swagger

万一您错过了它,我是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}'");

...没那么快

大团圆结局,每个人都赢吧?不完全的。如果您在 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<productdto> GetProduct(int id)</productdto>

该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<ProductDto> GetProduct(int id)

这产生了await clientApiProxy.GetProductAsync(productId);我所期望的。

结论

这篇文章是关于如何生成未经身份验证的客户端的故事。

同时,所有代码都可以在multi-api分支中运行,也可以在LeesStore演示站点的Multiple API的Pull Request中仔细阅读。我希望这是有帮助的。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。