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

目錄

更多Cowbell Swagger

探索ApiExplorer

消費Swagger

...沒那麼快

最後的技巧和竅門

結論


該博客條目通過向我現有的Web應用程序中添加第二個swagger文件,並控制其中的內容來進行。

最近,除了我的SPAAngular)應用使用的現有API之外,一位客戶要求我建立面向Web的小型最終用戶。

這似乎是一個很好的機會,可以寫博客介紹我的經驗,並與更多的讀者分享我的方法和解決方案的知識。

我當前的應用程序是基於帶有Angular模板的ASP.NET Boilerplate構建的。雖然這對這個故事來說並不重要,但重要的是,它是一個ASP.NET Core應用程序,Swashbuckle生成漂亮的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文件在: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,這是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的忠實粉絲。這是一個依賴管理工具(例如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<productdto> GetProduct(int id)</productdto>

ApiExplorer只露出了終點,而不是方法名。因此,Swashbuckle Swagger文件中不包含operationIdNSwag被迫使用端點中的元素來命名。

解決方法是指定名稱,以便Swashbuckle可以生成一個operationId。使用or 屬性中的屬性很容易。使用HttpGetHttpPost屬性中的Name屬性很容易做到這一點。多虧了C6中的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仔細閱讀。我希望這是有幫助的。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章