天天看點

使用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中仔細閱讀。我希望這是有幫助的。

繼續閱讀