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