目錄
更多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端口路由所涉及的工作量似乎太多了。
我想要更輕巧的替代方案,以擴充現有的安全模型并保留現有的配置。像這樣:

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