2016年2月16日 星期二

ASP.NET Web API - Import a Postman Collection Part.3 最後完成版

經過兩篇文章的鋪陳,總算來到了最後一篇,將會把修改過的最後完成版提供給各位,讓各位開發 Web API 專案時在操作使用 Postman 能夠方便與易於管理。

ASP.NET Web API - Import a Postman Collection Part.1
ASP.NET Web API - Import a Postman Collection Part.2

 


一開始還是要特別強調,程式的參考來源是以下的這兩篇文章:

Using ApiExplorer to export API information to PostMan, a Chrome extension for testing Web APIs - Yao's blog

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 都給移除,因為不會繼續沿用,雖然有些類別並沒有什麼差別,但還是請移除之前版本的程式碼(不這麼交代的話,恐怕之後一定會有很多人提問都會說「程式碼都跟你文章裡的一樣,為什麼還是有錯誤…」這幾年我已經看到數不清的類似提問)。

 

程式碼

image

首先一樣是在 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

image

 

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 Groups
            var 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 wrapped
            var postmanReadyUrl = this.pathVariableRegEx.Replace(cleanedUrlParameterUrl, ":$1");
 
            // prefix url with base uri
            var url = string.Concat(baseUri.TrimEnd('/'), "/", postmanReadyUrl);
 
            // Get Controller Action's Description
            var 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 資料內容,

image

image

上面的圖片裡可以看到 folders 內的每個 folder 資料,都有包含相關的 Request 的 id 編號,而這個就是 Folder 與 API 項目不會分開放置的關鍵,這也是我在 PostmanFeatures.cs 有特別去修改與重新整理的部分。

 

匯入

以下就直接來看 Postman 匯入後的結果,下圖可以看到每個 Folder 都是依據各個 Controller 去建立,Folder 名稱是會按照 Controller 類別上面的 XML Document 註解的內容來顯示,

image

開啟其中一個 Folder,可以看到相關的 API 項目都在 Folder 裡面

image

完成~

 


為什麼這個功能很重要呢?

在第一篇的一開始就有清楚地說明,除了要達到開發團隊在使用 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

 

參考資料

Using ApiExplorer to export API information to PostMan, a Chrome extension for testing Web APIs - Yao's blog

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

 

以上

沒有留言:

張貼留言

提醒

千萬不要使用 Google Talk (Hangouts) 或 Facebook 及時通訊與我聯繫、提問,因為會掉訊息甚至我是過了好幾天之後才發現到你曾經傳給我訊息過,請多多使用「詢問與建議」(在左邊,就在左邊),另外比較深入的問題討論,或是有牽涉到你實作程式碼的內容,不適合在留言板裡留言討論,請務必使用「詢問與建議」功能(可以夾帶檔案),謝謝。