經過兩篇文章的鋪陳,總算來到了最後一篇,將會把修改過的最後完成版提供給各位,讓各位開發 Web API 專案時在操作使用 Postman 能夠方便與易於管理。
ASP.NET Web API - Import a Postman Collection Part.1
ASP.NET Web API - Import a Postman Collection Part.2
一開始還是要特別強調,程式的參考來源是以下的這兩篇文章:
StackOverflow - How to generate JSON Postman Collections from a WebApi2 project using WebApi HelpPages that are suitable for import
http://stackoverflow.com/questions/23158379/how-to-generate-json-postman-collections-from-a-webapi2-project-using-webapi-hel/23158380#23158380
(引用、參考別人的文章就應該清楚交代)
在上一篇的最後執行結果看到了不合理的地方,API Collection 裡的每一個 API 項目都不在所屬的 Folder 內,而這些 Folder 是依據 Web API 裡 Controller 名稱所建立的,所以 Folder 裡應該要有該 Controller 的 API 項目,並不是分開的情況,以下就直接將修改過的程式碼分享出來。
修改過的程式碼與原版本有所出入,原則上我是希望將不會被變動的程式碼給抽離出來,如此一來這一個功能就可以獨立出來,之後可以做成 Packages 在往後開發的 Web API 專案裡使用。
前置作業
如果你有依照前兩篇文章跟著做的話,請將建立的類別、Controller 都給移除,因為不會繼續沿用,雖然有些類別並沒有什麼差別,但還是請移除之前版本的程式碼(不這麼交代的話,恐怕之後一定會有很多人提問都會說「程式碼都跟你文章裡的一樣,為什麼還是有錯誤…」這幾年我已經看到數不清的類似提問)。
程式碼
首先一樣是在 Models 裡的 Postman 資料夾裡建立以下三個類別:
PostmanRequestGet.cs
using System;using System.Collections.Generic;using Newtonsoft.Json;namespace WebApplication1.Models.Postman{ /// <summary> /// Class PostmanRequestGet. /// </summary>public class PostmanRequestGet
{ /// <summary> /// Gets or sets the collection identifier. /// </summary> /// <value>The collection identifier.</value> [JsonProperty(PropertyName = "collectionId")] public Guid CollectionId { get; set; } /// <summary> /// Gets or sets the identifier. /// </summary> /// <value>The identifier.</value> [JsonProperty(PropertyName = "id")] public Guid Id { get; set; } /// <summary> /// Gets or sets the headers. /// </summary> /// <value>The headers.</value> [JsonProperty(PropertyName = "headers")]public string Headers { get; set; }
/// <summary> /// Gets or sets the URL. /// </summary> /// <value>The URL.</value> [JsonProperty(PropertyName = "url")]public string Url { get; set; }
/// <summary> /// Gets or sets the path variables. /// </summary> /// <value>The path variables.</value> [JsonProperty(PropertyName = "pathVariables")]public Dictionary<string, string> PathVariables { get; set; }
/// <summary> /// Gets or sets the method. /// </summary> /// <value>The method.</value> [JsonProperty(PropertyName = "method")]public string Method { get; set; }
/// <summary> /// Gets or sets the data. /// </summary> /// <value>The data.</value> [JsonProperty(PropertyName = "data")]public string Data { get; set; }
/// <summary> /// Gets or sets the data mode. /// </summary> /// <value>The data mode.</value> [JsonProperty(PropertyName = "dataMode")]public string DataMode { get; set; }
/// <summary> /// Gets or sets the name. /// </summary> /// <value>The name.</value> [JsonProperty(PropertyName = "name")]public string Name { get; set; }
/// <summary> /// Gets or sets the description. /// </summary> /// <value>The description.</value> [JsonProperty(PropertyName = "description")]public string Description { get; set; }
/// <summary> /// Gets or sets the description format. /// </summary> /// <value>The description format.</value> [JsonProperty(PropertyName = "descriptionFormat")]public string DescriptionFormat { get; set; }
/// <summary> /// Gets or sets the time. /// </summary> /// <value>The time.</value> [JsonProperty(PropertyName = "time")]public long Time { get; set; }
/// <summary> /// Gets or sets the version. /// </summary> /// <value>The version.</value> [JsonProperty(PropertyName = "version")]public string Version { get; set; }
/// <summary> /// Gets or sets the responses. /// </summary> /// <value>The responses.</value> [JsonProperty(PropertyName = "responses")]public ICollection<string> Responses { get; set; }
/// <summary> /// Gets or sets a value indicating whether this <see cref="PostmanRequestGet"/> is synced. /// </summary> /// <value><c>true</c> if synced; otherwise, <c>false</c>.</value> [JsonProperty(PropertyName = "synced")]public bool Synced { get; set; }
}
}
PostmanFolderGet.cs
using System;using System.Collections.Generic;using Newtonsoft.Json;namespace WebApplication1.Models.Postman{ /// <summary> /// Class PostmanFolderGet. /// </summary>public class PostmanFolderGet
{ /// <summary> /// Gets or sets the identifier. /// </summary> /// <value>The identifier.</value> [JsonProperty(PropertyName = "id")] public Guid Id { get; set; } /// <summary> /// Gets or sets the name. /// </summary> /// <value>The name.</value> [JsonProperty(PropertyName = "name")]public string Name { get; set; }
/// <summary> /// Gets or sets the description. /// </summary> /// <value>The description.</value> [JsonProperty(PropertyName = "description")]public string Description { get; set; }
/// <summary> /// Gets or sets the order. /// </summary> /// <value>The order.</value> [JsonProperty(PropertyName = "order")] public ICollection<Guid> Order { get; set; } /// <summary> /// Gets or sets the name of the collection. /// </summary> /// <value>The name of the collection.</value> [JsonProperty(PropertyName = "collection_name")]public string CollectionName { get; set; }
/// <summary> /// Gets or sets the collection identifier. /// </summary> /// <value>The collection identifier.</value> [JsonProperty(PropertyName = "collection_id")] public Guid CollectionId { get; set; }}
}
PostmanCollectionGet.cs
using System;using System.Collections.Generic;using Newtonsoft.Json;namespace WebApplication1.Models.Postman{ /// <summary> /// Class PostmanCollectionGet. /// </summary>public class PostmanCollectionGet
{ /// <summary> /// Gets or sets the identifier. /// </summary> /// <value>The identifier.</value> [JsonProperty(PropertyName = "id")] public Guid Id { get; set; } /// <summary> /// Gets or sets the name. /// </summary> /// <value>The name.</value> [JsonProperty(PropertyName = "name")]public string Name { get; set; }
/// <summary> /// Gets or sets the description. /// </summary> /// <value>The description.</value> [JsonProperty(PropertyName = "description")]public string Description { get; set; }
/// <summary> /// Gets or sets the order. /// </summary> /// <value>The order.</value> [JsonProperty(PropertyName = "order")] public ICollection<Guid> Order { get; set; } /// <summary> /// Gets or sets the folders. /// </summary> /// <value>The folders.</value> [JsonProperty(PropertyName = "folders")] public ICollection<PostmanFolderGet> Folders { get; set; } /// <summary> /// Gets or sets the timestamp. /// </summary> /// <value>The timestamp.</value> [JsonProperty(PropertyName = "timestamp")]public long Timestamp { get; set; }
/// <summary> /// Gets or sets a value indicating whether this <see cref="PostmanCollectionGet"/> is synced. /// </summary> /// <value><c>true</c> if synced; otherwise, <c>false</c>.</value> [JsonProperty(PropertyName = "synced")]public bool Synced { get; set; }
/// <summary> /// Gets or sets the requests. /// </summary> /// <value>The requests.</value> [JsonProperty(PropertyName = "requests")] public ICollection<PostmanRequestGet> Requests { get; set; }}
}
接著把上一篇建立在 PostmanApiController.cs 內的程式碼給抽離出來,並非只是單純的將程式抽離而已,也把程式碼做了整理。
在專案根目錄下建立「Infrastructure」資料夾(一般我會將一些基礎類別或是功能類別依據分類放在這個目錄裡),在 Infrastructure 下建立「Postman」資料夾,分別建立兩個類別:
HttpMethodComparator.cs
PostmanFeature.cs
HttpMethodComparator.cs
using System;using System.Collections.Generic;using System.Linq;using System.Net.Http;using System.Web;namespace WebApplication1.Infrastructure.Postman{ /// <summary> /// Quick comparer for ordering http methods for display. /// </summary>internal class HttpMethodComparator : IComparer<HttpMethod>
{ /// <summary> /// The order /// </summary>private readonly string[] order =
{ "GET", "POST", "PUT", "DELETE", "PATCH"};
/// <summary> /// Compares the specified x. /// </summary> /// <param name="x">The x.</param> /// <param name="y">The y.</param> /// <returns>System.Int32.</returns>public int Compare(HttpMethod x, HttpMethod y)
{return Array.IndexOf(this.order, x.ToString())
.CompareTo(Array.IndexOf(this.order, y.ToString()));}
}
}
PostmanFeatures.cs
using System;using System.Collections.Generic;using System.Collections.ObjectModel;using System.Linq;using System.Net.Http;using System.Net.Http.Headers;using System.Text.RegularExpressions;using System.Web;using System.Web.Http;using System.Web.Http.Controllers;using System.Web.Http.Description;using WebApplication1.Areas.HelpPage;using WebApplication1.Models.Postman;namespace WebApplication1.Infrastructure.Postman{ /// <summary> /// Class PostmanFeatures. /// </summary>public class PostmanFeatures
{private readonly Regex pathVariableRegEx = new Regex(
"\\{([A-Za-z0-9-_]+)\\}", RegexOptions.ECMAScript | RegexOptions.Compiled);private readonly Regex urlParameterVariableRegEx = new Regex(
"=\\{([A-Za-z0-9-_]+)\\}", RegexOptions.ECMAScript | RegexOptions.Compiled);private string CollectionName { get; set; }
private string CollectionDesc { get; set; }
private string NameSpaceFullName { get; set; }
public PostmanFeatures(string collectionName,
string collectionDesc, string nameSpaceFullName) { this.CollectionName = collectionName; this.CollectionDesc = collectionDesc; this.NameSpaceFullName = nameSpaceFullName;}
//----------------------------------------------------------------------------------------- public PostmanCollectionGet PostmanCollectionForController(HttpRequestMessage request) {var requestUri = request.RequestUri;
var baseUri = requestUri.Scheme + "://" + requestUri.Host + ":" + requestUri.Port +
HttpContext.Current.Request.ApplicationPath;
var postManCollection = new PostmanCollectionGet {Id = Guid.NewGuid(),
Name = this.CollectionName, Description = this.CollectionDesc, Order = new Collection<Guid>(), Folders = new Collection<PostmanFolderGet>(),Timestamp = DateTime.Now.Ticks,
Synced = false, Requests = new Collection<PostmanRequestGet>()};
var configuration = request.GetConfiguration();
var helpPageSampleGenerator = configuration.GetHelpPageSampleGenerator();
var apiExplorer = configuration.Services.GetApiExplorer();
var apiDescriptionsByController =
apiExplorer.ApiDescriptions
.Where(x => !x.ActionDescriptor.ControllerDescriptor.ControllerName.Contains("Error")).GroupBy(x => x.ActionDescriptor.ActionBinding.ActionDescriptor.ControllerDescriptor.ControllerType);
// API Groupsvar apiDescriptions = configuration.Services.GetApiExplorer().ApiDescriptions;
ILookup<HttpControllerDescriptor, ApiDescription> apiGroups =
apiDescriptions.ToLookup(api => api.ActionDescriptor.ControllerDescriptor);
var documentationProvider = configuration.Services.GetDocumentationProvider();
foreach (var apiDescriptionsByControllerGroup in apiDescriptionsByController)
{ this.ApiControllers(apiDescriptionsByControllerGroup: apiDescriptionsByControllerGroup,
postManCollection: postManCollection,
helpPageSampleGenerator: helpPageSampleGenerator,
apiGroups: apiGroups,
documentationProvider: documentationProvider,
baseUri: baseUri,
nameSpaceFullName: this.NameSpaceFullName);}
return postManCollection;}
/// <summary> /// APIs the folders. /// </summary> /// <param name="apiDescriptionsByControllerGroup">The API descriptions by controller group.</param> /// <param name="postManCollection">The post man collection.</param> /// <param name="helpPageSampleGenerator">The help page sample generator.</param> /// <param name="baseUri">The base URI.</param> /// <param name="nameSpaceFullName">Full name of the name space.</param>private void ApiControllers(
IGrouping<Type, ApiDescription> apiDescriptionsByControllerGroup,
PostmanCollectionGet postManCollection,
HelpPageSampleGenerator helpPageSampleGenerator,
ILookup<HttpControllerDescriptor, ApiDescription> apiGroups,
IDocumentationProvider documentationProvider,
string baseUri, string nameSpaceFullName) {var controllerName =
apiDescriptionsByControllerGroup.Key.Name.Replace("Controller", string.Empty);
var apiGroup =
apiGroups.Where(x => x.Key.ControllerName.Contains(controllerName)).FirstOrDefault();
var controllerDocumentation =
documentationProvider.GetDocumentation(apiGroup.Key);
var controllerDisplayName = controllerDocumentation;
var folderName = string.IsNullOrWhiteSpace(controllerDisplayName)? controllerName
: controllerDisplayName;
var postManFolder = new PostmanFolderGet {Id = Guid.NewGuid(),
CollectionId = postManCollection.Id,
Name = folderName,
Description = string.Format("Api Methods for {0}", controllerName),
CollectionName = "api", Order = new Collection<Guid>()};
var apiDescriptions =
apiDescriptionsByControllerGroup.OrderBy(description => description.HttpMethod, new HttpMethodComparator()).ThenBy(description => description.RelativePath);
ICollection<Guid> requestGuids = new Collection<Guid>();foreach (var apiDescription in apiDescriptions)
{if (!postManFolder.Name.Equals("Error", StringComparison.OrdinalIgnoreCase))
{ var request = this.ApiActions(postManCollection, helpPageSampleGenerator,
baseUri, apiDescription, nameSpaceFullName);
requestGuids.Add(request.Id);
postManCollection.Requests.Add(request);
}
}
foreach (var guid in requestGuids)
{postManFolder.Order.Add(guid);
}
postManCollection.Folders.Add(postManFolder);
}
private PostmanRequestGet ApiActions(PostmanCollectionGet postManCollection,HelpPageSampleGenerator helpPageSampleGenerator,
string baseUri,ApiDescription apiDescription,
string nameSpaceFullName) { TextSample sampleData = null;var sampleDictionary =
helpPageSampleGenerator.GetSample(apiDescription, SampleDirection.Request);
MediaTypeHeaderValue mediaTypeHeader;
if (MediaTypeHeaderValue.TryParse("application/json", out mediaTypeHeader) &&
sampleDictionary.ContainsKey(mediaTypeHeader))
{ sampleData = sampleDictionary[mediaTypeHeader] as TextSample;}
// scrub curly braces from url parameter values var cleanedUrlParameterUrl = this.urlParameterVariableRegEx.Replace( apiDescription.RelativePath, "=$1-value"); // get pat variables from url var pathVariables = this.pathVariableRegEx.Matches(cleanedUrlParameterUrl).Cast<Match>()
.Select(m => m.Value)
.Select(s => s.Substring(1, s.Length - 2))
.ToDictionary(s => s, s => string.Format("{0}-value", s));
// change format of parameters within string to be colon prefixed rather than curly brace wrappedvar postmanReadyUrl = this.pathVariableRegEx.Replace(cleanedUrlParameterUrl, ":$1");
// prefix url with base urivar url = string.Concat(baseUri.TrimEnd('/'), "/", postmanReadyUrl);
// Get Controller Action's Descriptionvar actionDisplayName = apiDescription.Documentation;
var requestDescription = apiDescription.RelativePath;
var request = new PostmanRequestGet {CollectionId = postManCollection.Id,
Id = Guid.NewGuid(),
Name = actionDisplayName,
Description = requestDescription,
Url = url,
Method = apiDescription.HttpMethod.Method,
Headers = "Content-Type: application/json",Data = sampleData == null ? "" : sampleData.Text,
DataMode = "raw",Time = postManCollection.Timestamp,
Synced = false, DescriptionFormat = "markdown", Version = "beta",Responses = new Collection<string>(),
PathVariables = pathVariables
};
return request;}
}
}
最後就是在 Controllers 裡建立 PostmanApiController.cs
[ApiExplorerSettings(IgnoreApi = true)][RoutePrefix("api/postmanapi")]public class PostmanApiController : ApiController
{private readonly string collectionName = "WebApplication1 - WebAPI";
private readonly string collectionDesc = "WebApplication1 - 範例程式";
private readonly string nameSpaceFullName = "WebApplication1.Controllers";
public PostmanApiController() {}
[HttpGet]
[Route(Name = "GetPostmanCollection")] public HttpResponseMessage GetPostmanCollection() {var postmanFeatures =
new PostmanFeatures(collectionName, collectionDesc, nameSpaceFullName);var postmancollection =
postmanFeatures.PostmanCollectionForController(this.Request); return Request.CreateResponse<PostmanCollectionGet>( HttpStatusCode.OK, postmancollection, "application/json");}
}
這裡的 PostmanApiController 與前一個版本的內容已經有很大的不同了,因為是已經把匯出 Postman API Collection 的程式給移到 PostmanFeatures.cs 裡,另外將 Collection Name, Description 等會容易變動的部分給保留在 PostmanApiController 裡,這麼一來就不需要一再地去更動到 PostmanFeatures.cs 的程式內容。
執行
一樣是依照前面裡兩篇文章最後的步驟,重新建置、執行,先在瀏覽器裡輸入網址「localhost:20620/api/postmanapi」,先查看輸出的 API Collection JSON 資料內容,
上面的圖片裡可以看到 folders 內的每個 folder 資料,都有包含相關的 Request 的 id 編號,而這個就是 Folder 與 API 項目不會分開放置的關鍵,這也是我在 PostmanFeatures.cs 有特別去修改與重新整理的部分。
匯入
以下就直接來看 Postman 匯入後的結果,下圖可以看到每個 Folder 都是依據各個 Controller 去建立,Folder 名稱是會按照 Controller 類別上面的 XML Document 註解的內容來顯示,
開啟其中一個 Folder,可以看到相關的 API 項目都在 Folder 裡面
完成~
為什麼這個功能很重要呢?
在第一篇的一開始就有清楚地說明,除了要達到開發團隊在使用 Postman 時的 API Collection 一致性之外,另外就是像這種在 Postman 建立 API Collection 與 Request 的工作不應該以人工的方式去建立,應該要建立這樣一個可以產生 Postman 匯入格式的功能來取代原有的方式。
另外還有一點就是可以提供給其他開發團隊使用,尤其是 APP 開發團隊,他們對於這樣的功能是相當歡迎的,畢竟也都是使用 Postman 去對 Web API 做測試、操作,所以如果有一個功能是他們不需要自行手動建立 API Collection 的話,對他們來說是一件相當方便、省事的。
相關文章
ASP.NET Web API - Import a Postman Collection Part.1
ASP.NET Web API - Import a Postman Collection Part.2
參考資料
StackOverflow - How to generate JSON Postman Collections from a WebApi2 project using WebApi HelpPages that are suitable for import
http://stackoverflow.com/questions/23158379/how-to-generate-json-postman-collections-from-a-webapi2-project-using-webapi-hel/23158380#23158380
以上
沒有留言:
張貼留言