天天看点

使用Swagger,ApiExplorer和NSwag掌握ASP.NET Core和ABP中的外部Web API更多Cowbell Swagger探索ApiExplorer消费Swagger...没那么快最后的技巧和窍门结论

目录

更多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端口路由所涉及的工作量似乎太多了。

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

使用Swagger,ApiExplorer和NSwag掌握ASP.NET Core和ABP中的外部Web API更多Cowbell Swagger探索ApiExplorer消费Swagger...没那么快最后的技巧和窍门结论

更多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文件在: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文件之间进行选择:

使用Swagger,ApiExplorer和NSwag掌握ASP.NET Core和ABP中的外部Web API更多Cowbell Swagger探索ApiExplorer消费Swagger...没那么快最后的技巧和窍门结论

很好,对吗?

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

探索ApiExplorer

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

消费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}'");
           

...没那么快

大团圆结局,每个人都赢吧?不完全的。如果您在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消息返回给用户。

使用Swagger,ApiExplorer和NSwag掌握ASP.NET Core和ABP中的外部Web API更多Cowbell Swagger探索ApiExplorer消费Swagger...没那么快最后的技巧和窍门结论

上面的屏幕截图来自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中仔细阅读。我希望这是有帮助的。

继续阅读