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

接續上一篇的內容,來看看最原始的做法有些什麼樣的問題,先開門見山做個說明,這一篇會再沿用別人所提供的改良做法,不過最後的結果依舊還是會有一些問題存在,所以最後在第三篇的文章裡將會提供我所修改過的程式碼,而這也是我開發的專案以及部門其他開發團隊所使用的方法。

在上一篇的最後有說到,原始的做法雖然可以完成把 Web API 的 API Collection 匯出成 Postman 可匯入的 JSON 資料,但是所有的 API 項目都全部列在 Collection 裡面,在專案開發越來越多的服務時,API 項目會一直增加,到時候要在一堆的 API 項目裡找出我們想要的那一個就要花點功夫與眼力。

image

所以比較好的做法應該是依據 Controller 去建立個別的 Folder,讓後再將 API 項目放到所屬的 Controller Folder 內。

 

還有另外一點就是在有參數的 API 項目裡,無法將放在 URL 內的參數轉換放置到 Postman 的 Params 列表裡,不知道我在說什麼的就看下面的圖示說明,

image

如果 URL 內的參數格式正確的話,那麼是可以將 URL Parameter 轉換放置到 Params 列表,

image

 

參考做法

其實在「Using ApiExplorer to export API information to PostMan, a Chrome extension for testing Web APIs - Yao's blog」這篇文章底下的討論裡就有人提供了修改的做法,

image

由 Robert Engelhardt 在 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

 

修改

將前一篇的做法依據上面的內容做出修改,

Models 建立三個類別:PostmanCollectionGet, PostmanFolderGet, PostmanRequest

image

PostmanCollectionGet.cs

using System;
using System.Collections.Generic;
using Newtonsoft.Json;

namespace WebApplication1.Models.Postman
{
    /// <summary>
    ///     [Postman](http://getpostman.com) collection representation
    /// </summary>
    public class PostmanCollectionGet
    {
        /// <summary>
        ///     Id of collection
        /// </summary>
        [JsonProperty(PropertyName = "id")]
        public Guid Id { get; set; }

        /// <summary>
        ///     Name of collection
        /// </summary>
        [JsonProperty(PropertyName = "name")]
        public string Name { get; set; }

        /// <summary>
        ///     Collection generation time
        /// </summary>
        [JsonProperty(PropertyName = "timestamp")]
        public long Timestamp { get; set; }

        /// <summary>
        ///     Requests associated with the collection
        /// </summary>
        [JsonProperty(PropertyName = "requests")]
        public ICollection<PostmanRequestGet> Requests { get; set; }

        /// <summary>
        ///     **unused always false**
        /// </summary>
        [JsonProperty(PropertyName = "synced")]
        public bool Synced { get; set; }

        /// <summary>
        ///     folders within the collection
        /// </summary>
        [JsonProperty(PropertyName = "folders")]
        public ICollection<PostmanFolderGet> Folders { get; set; }

        /// <summary>
        ///     Description of collection
        /// </summary>
        [JsonProperty(PropertyName = "description")]
        public string Description { get; set; }
    }
}

PostmanFolderGet.cs

using System;
using System.Collections.Generic;
using Newtonsoft.Json;

namespace WebApplication1.Models.Postman
{
    /// <summary>
    ///     Object that describes a [Postman](http://getpostman.com) folder
    /// </summary>
    public class PostmanFolderGet
    {
        /// <summary>
        ///     id of the folder
        /// </summary>
        [JsonProperty(PropertyName = "id")]
        public Guid Id { get; set; }

        /// <summary>
        ///     folder name
        /// </summary>
        [JsonProperty(PropertyName = "name")]
        public string Name { get; set; }

        /// <summary>
        ///     folder description
        /// </summary>
        [JsonProperty(PropertyName = "description")]
        public string Description { get; set; }

        /// <summary>
        ///     ordered list of ids of items in folder
        /// </summary>
        [JsonProperty(PropertyName = "order")]
        public ICollection<Guid> Order { get; set; }

        /// <summary>
        ///     Name of the collection
        /// </summary>
        [JsonProperty(PropertyName = "collection_name")]
        public string CollectionName { get; set; }

        /// <summary>
        ///     id of the collection
        /// </summary>
        [JsonProperty(PropertyName = "collection_id")]
        public Guid CollectionId { get; set; }
    }
}

PostmanRequestGet.cs

using System;
using System.Collections.Generic;
using Newtonsoft.Json;

namespace WebApplication1.Models.Postman
{
    /// <summary>
    ///     [Postman](http://getpostman.com) request object
    /// </summary>
    public class PostmanRequestGet
    {
        /// <summary>
        ///     id of request
        /// </summary>
        [JsonProperty(PropertyName = "id")]
        public Guid Id { get; set; }

        /// <summary>
        ///     headers associated with the request
        /// </summary>
        [JsonProperty(PropertyName = "headers")]
        public string Headers { get; set; }

        /// <summary>
        ///     url of the request
        /// </summary>
        [JsonProperty(PropertyName = "url")]
        public string Url { get; set; }

        /// <summary>
        ///     path variables of the request
        /// </summary>
        [JsonProperty(PropertyName = "pathVariables")]
        public Dictionary<string, string> PathVariables { get; set; }

        /// <summary>
        ///     method of request
        /// </summary>
        [JsonProperty(PropertyName = "method")]
        public string Method { get; set; }

        /// <summary>
        ///     data to be sent with the request
        /// </summary>
        [JsonProperty(PropertyName = "data")]
        public string Data { get; set; }

        /// <summary>
        ///     data mode of reqeust
        /// </summary>
        [JsonProperty(PropertyName = "dataMode")]
        public string DataMode { get; set; }

        /// <summary>
        ///     name of request
        /// </summary>
        [JsonProperty(PropertyName = "name")]
        public string Name { get; set; }

        /// <summary>
        ///     request description
        /// </summary>
        [JsonProperty(PropertyName = "description")]
        public string Description { get; set; }

        /// <summary>
        ///     format of description
        /// </summary>
        [JsonProperty(PropertyName = "descriptionFormat")]
        public string DescriptionFormat { get; set; }

        /// <summary>
        ///     time that this request object was generated
        /// </summary>
        [JsonProperty(PropertyName = "time")]
        public long Time { get; set; }

        /// <summary>
        ///     version of the request object
        /// </summary>
        [JsonProperty(PropertyName = "version")]
        public string Version { get; set; }

        /// <summary>
        ///     request response
        /// </summary>
        [JsonProperty(PropertyName = "responses")]
        public ICollection<string> Responses { get; set; }

        /// <summary>
        ///     the id of the collection that the request object belongs to
        /// </summary>
        [JsonProperty(PropertyName = "collectionId")]
        public Guid CollectionId { get; set; }

        /// <summary>
        ///     Synching
        /// </summary>
        [JsonProperty(PropertyName = "synced")]
        public bool Synced { get; set; }
    }
}

 

另外在 Controllers 裡建立 PostmanApiController.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
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.Description;
using WebApplication1.Areas.HelpPage;
using WebApplication1.Models.Postman;

namespace WebApplication1.Controllers
{
    /// <summary>
    /// Based on
    /// http://blogs.msdn.com/b/yaohuang1/archive/2012/06/15/using-apiexplorer-to-export-api-information-to-postman-a-chrome-extension-for-testing-web-apis.aspx
    /// </summary>
    [ApiExplorerSettings(IgnoreApi = true)]
    [RoutePrefix("api/postman")]
    public class PostmanApiController : ApiController
    {
        /// <summary>
        /// Produce [POSTMAN](http://www.getpostman.com) related responses
        /// </summary>
        public PostmanApiController()
        {
            // exists for documentation purposes
        }

        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);

        /// <summary>
        /// Get a postman collection of all visible Api
        /// (Get the [POSTMAN](http://www.getpostman.com) chrome extension)
        /// </summary>
        /// <returns>object describing a POSTMAN collection</returns>
        /// <remarks>Get a postman collection of all visible api</remarks>
        [HttpGet]
        [Route(Name = "GetPostmanCollection")]
        [ResponseType(typeof(PostmanCollectionGet))]
        public IHttpActionResult GetPostmanCollection()
        {
            return Ok(this.PostmanCollectionForController());
        }

        private PostmanCollectionGet PostmanCollectionForController()
        {
            var requestUri = Request.RequestUri;
            var baseUri = requestUri.Scheme + "://" + requestUri.Host + ":" + requestUri.Port
                          + HttpContext.Current.Request.ApplicationPath;

            var postManCollection =
                new PostmanCollectionGet
                {
                    Id = Guid.NewGuid(),
                    Name = "[Name of your API]",
                    Timestamp = DateTime.Now.Ticks,
                    Requests = new Collection<PostmanRequestGet>(),
                    Folders = new Collection<PostmanFolderGet>(),
                    Synced = false,
                    Description = "[Description of your API]"
                };

            var helpPageSampleGenerator = Configuration.GetHelpPageSampleGenerator();

            var apiExplorer = Configuration.Services.GetApiExplorer();

            var apiDescriptionsByController = apiExplorer.ApiDescriptions.GroupBy(
                description =>
                    description.ActionDescriptor.ActionBinding.ActionDescriptor.ControllerDescriptor.ControllerType);

            foreach (var apiDescriptionsByControllerGroup in apiDescriptionsByController)
            {
                var controllerName =
                    apiDescriptionsByControllerGroup.Key.Name.Replace("Controller", string.Empty);

                var postManFolder =
                    new PostmanFolderGet
                    {
                        Id = Guid.NewGuid(),
                        CollectionId = postManCollection.Id,
                        Name = controllerName,
                        Description = string.Format("Api Methods for {0}", controllerName),
                        CollectionName = "api",
                        Order = new Collection<Guid>()
                    };

                foreach (var apiDescription in apiDescriptionsByControllerGroup
                    .OrderBy(description => description.HttpMethod, new HttpMethodComparator())
                    .ThenBy(description => description.RelativePath)
                    .ThenBy(description => description.Documentation.ToString(CultureInfo.InvariantCulture)))
                {
                    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 = baseUri.TrimEnd('/') + "/" + postmanReadyUrl;

                    var request = new PostmanRequestGet
                                  {
                                      CollectionId = postManCollection.Id,
                                      Id = Guid.NewGuid(),
                                      Name = apiDescription.RelativePath,
                                      Description = apiDescription.Documentation,
                                      Url = url,
                                      Method = apiDescription.HttpMethod.Method,
                                      Headers = "Content-Type: application/json",
                                      Data = sampleData == null
                                          ? null
                                          : sampleData.Text,
                                      DataMode = "raw",
                                      Time = postManCollection.Timestamp,
                                      Synced = false,
                                      DescriptionFormat = "markdown",
                                      Version = "beta",
                                      Responses = new Collection<string>(),
                                      PathVariables = pathVariables
                                  };

                    postManFolder.Order.Add(request.Id); // add to the folder
                    postManCollection.Requests.Add(request);
                }

                postManCollection.Folders.Add(postManFolder);
            }

            return postManCollection;
        }
    }

    /// <summary>
    ///     Quick comparer for ordering http methods for display
    /// </summary>
    internal class HttpMethodComparator : IComparer<HttpMethod>
    {
        private readonly string[] _order =
        {
            "GET",
            "POST",
            "PUT",
            "DELETE"
        };

        public int Compare(HttpMethod x, HttpMethod y)
        {
            return Array.IndexOf(this._order, x.ToString())
                        .CompareTo(Array.IndexOf(this._order, y.ToString()));
        }
    }
}

 

Postman - Import From URL

建立完成並且重新建置專案後執行,Web API 專案的執行 URL 使用 Postman,例如「localhost:20620/api/postman」,應該可以看到輸出的 API Collection 的 JSON 格式的資料內容,

image

 

開啟 Postman 然後使用 Import From URL 功能將 API Collection 匯入到 Postman裡,不過這邊卻會遇到一點問題,

image

這是匯出的 JSON 資料裡,每一個 Request 都必須要有 CollectionId 欄位,但是我們現在所輸出的 JSON 資料裡每個 Request 內的欄位是有「collection-id」欄位卻沒有「collectionId」,所以就需要修改 PostmanRequestGet 類別 CollectionId 的 JsonProperty 裡的 PropertyName 值,

image

另外,這邊建議 PostmanApiController.cs 加上 ApiExplorerSettings Attribute,然後將 IgnoreApi 設定為 true,設定為 true 就表示在輸出時就會忽略掉這個 Controller 內的公開 API 項目。

image

 

重新建置、執行之後再做一次匯入,這時候就應該可以匯入完成,

image

 

檢視匯入完成後的 API Collection,可以看到依照 Controller 名稱所建立的 Folder

image

但是…

再往下看,就會看到有一堆的 API 項目列在那邊,而不是放在所屬的 Folder 裡面…

image

 

不過另一方面,前面有提到過的 URL 裡參數需要可以轉換列在 Params 列表裡,這一個版本就有解決這個問題。

接下來就是要解決 Folder 與 API 項目分開的問題,總不能每次更新匯入後就要我們自己手動去把 API 項目一個個拉到 Folder 裡,所以這個問題在下一篇文章裡將會告訴大家怎麼解決。

 


 

參考資料

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

 

以上

 

 

 

 

 

 

 

純粹是在寫興趣的,用寫程式、寫文章來抒解工作壓力