目錄
該博客條目通過向我現有的Web應用程序中添加第二個swagger文件,並控制其中的內容來進行。
最近,除了我的SPA(Angular)應用使用的現有API之外,一位客戶要求我建立面向Web的小型最終用戶。
這似乎是一個很好的機會,可以寫博客介紹我的經驗,並與更多的讀者分享我的方法和解決方案的知識。
我當前的應用程序是基於帶有Angular模板的ASP.NET Boilerplate構建的。雖然這對這個故事來說並不重要,但重要的是,它是一個ASP.NET Core應用程序,Swashbuckle(“生成漂亮的API文檔”的工具)將在其中生成Swagger文檔。
最初,我曾考慮向部署了我的站點的Kubernetes集羣添加一個額外的微服務。問題是新API很小,並且涉及設置安全性、DI、日誌記錄、應用程序設置、配置、docker和Kubernetes端口路由所涉及的工作量似乎太多了。
我想要更輕巧的替代方案,以擴展現有的安全模型並保留現有的配置。像這樣:
更多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的忠實粉絲。這是一個依賴管理工具(例如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消息返回給用戶。
上面的屏幕截圖來自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中仔細閱讀。我希望這是有幫助的。