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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Description;
using WebApplication1.Infrastructure.Postman;
using WebApplication1.Models.Postman;

namespace WebApplication1.Controllers
{
    [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

 

以上

 

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