博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
使用Swagger,ApiExplorer和NSwag掌握ASP.NET Core和ABP中的外部Web API
阅读量:3528 次
发布时间:2019-05-20

本文共 5433 字,大约阅读时间需要 18 分钟。

目录


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

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

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

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

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

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

更多Cowbell Swagger

向我现有的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文件之间进行选择

很好,对吗?

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

探索ApiExplorer

为了解决这个问题,我需要深入研究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获胜。

消费Swagger

万一您错过了它,我是。这是一个依赖管理工具(例如MakeRakeMavenGruntGulp),可以使用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 CreateProxybuild.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文件中不包含operationIdNSwag被迫使用端点中的元素来命名。

解决方法是指定名称,以便Swashbuckle可以生成一个operationId。使用or 属性中的属性很容易。使用HttpGetHttpPost属性中的Name属性很容易做到这一点。多亏了C6中的nameof,我们可以使它保持强类型。

[HttpGet("api/client/v1/product/{id}", Name = nameof(GetProduct))]public async Task
GetProduct(int id)

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

结论

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

同时,所有代码都可以在multi 分支中运行,也可以在演示站点的 Pull Request。我希望这是有帮助的。

 

转载地址:http://gbwhj.baihongyu.com/

你可能感兴趣的文章
Mysql利用注解进行开发
查看>>
Mybatis一对多查询,多对一查询
查看>>
Spring配置bean.xml文件的头目录模板
查看>>
代理模式之------动态代理
查看>>
Spring实现AOP的三种方式
查看>>
Mybatis-Spring简单的配置和使用,配置事务
查看>>
SpringMVC和Mybatis整合使用的配置文件
查看>>
代码特效插件pycharm
查看>>
python实现tcp客户端从服务端下载文件
查看>>
将字符串 “k:1|k1:2|k2:3|k3:4” 转换成字典{“k”:1,”k1”:2,”k2”:3,”k3”:4}
查看>>
AttributeError: 'tuple' object has no attribute 'decode'
查看>>
node爬虫(牛刀小试)
查看>>
关于vue的seo优化
查看>>
字符串在html中的页面中的换行
查看>>
react父子组件间的通信和传值
查看>>
vue-cli3.0设置环境变量
查看>>
vue父组件直接操作子组件的方法(不通过$emit和$on)
查看>>
vue上传文件到UCloud
查看>>
获取input选择文件的本地地址
查看>>
React绑定全局方法或变量
查看>>