經過兩篇文章的鋪陳,總算來到了最後一篇,將會把修改過的最後完成版提供給各位,讓各位開發 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
以上
 
沒有留言:
張貼留言