Postman是一个可以用来轻松测试 RESTful Web 服务的工具。
如果 Asp.Net 项目将WebApi与 WebApi 结合使用,则可以为暴露的 RESTful Web 服务自动生成帮助页面文档。
这个自动生成的文档很好,但可以通过增加可访问性使其变得更好。
如何将这些技术结合起来生成可以在 Postman 中导入的 JSON 文件?
扩展博客文章“使用 ApiExplorer 将 API 信息导出到 PostMan,一个用于测试 Web API 的 Chrome 扩展”,可以生成一个 JSON 文件,该文件可以导入 Postman 以用于测试和记录。
首先你需要设置一个可以导出 JSON 的控制器
/// <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>
[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()));
}
}
并生成正确的模型:
一个用于PostManCollection
/// <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; }
}
一个用于邮递员文件夹
/// <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; }
}
最后是PostmanRequest的模型
/// <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 = "collection-id")]
public Guid CollectionId { get; set; }
/// <summary>
/// Synching
/// </summary>
[JsonProperty(PropertyName = "synced")]
public bool Synced { get; set; }
}
现在您需要做的就是向 [application]api/postman 发出一个 GET 请求,您将拥有邮递员可读的最新的 restful API。
为什么不使用标准 Swagger 并将其与 Postman 一起使用?
- 什么是招摇?(Rest Web API 文档和客户端启用程序)
- 将 Swagger 文件导入 Postman
- 在 Visual Studio 中使用Swashbuckle NuGet 包为您的 API 生成 Swagger (Install-Package Swashbuckle -Pre)
奖励:ASP.NET Core Rest WebAPI 支持此解决方案
使用 .net core 2.2 的示例IActionDescriptorCollectionProvider
,基于邮递员模式:https ://schema.getpostman.com/json/collection/v2.0.0/collection.json
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using GR.Core.Extensions;
using GR.Core.Razor.Attributes;
using GR.Core.Razor.Models.PostmanModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal;
namespace GR.Core.Razor.Api
{
[AllowAnonymous]
[Route("/postman")]
public class PostmanDocsApiController : Controller
{
#region Injectable
/// <summary>
/// Inject action descriptor service
/// </summary>
private readonly IActionDescriptorCollectionProvider _provider;
#endregion
public PostmanDocsApiController(IActionDescriptorCollectionProvider provider)
{
_provider = provider;
}
/// <summary>
/// Postman collection
/// </summary>
/// <returns></returns>
[HttpGet]
[JsonProduces(typeof(PostmanCollection))]
public JsonResult Docs()
{
var postManCollection = new PostmanCollection
{
Info = new PostmanInfo
{
Id = Guid.NewGuid(),
Name = $"{GearApplication.ApplicationName} API",
Description = "api"
},
Folders = new Collection<PostmanFolder>()
};
var apiRoutes = _provider.ActionDescriptors.Items
.Where(x => x.AttributeRouteInfo?.Template?.StartsWith("api/") ?? false).ToList();
var groups = apiRoutes.GroupBy(x => x.RouteValues["Controller"])
.ToList();
foreach (var group in groups)
{
var controllerGroup = apiRoutes.FirstOrDefault(x => x.RouteValues["Controller"].Equals(group.Key)).Is<ControllerActionDescriptor>();
var type = controllerGroup.ControllerTypeInfo;
var typeSummary = type.GetSummary();
var postManFolder = new PostmanFolder
{
Name = group.Key,
Description = typeSummary,
FolderRequests = new List<PostmanFolderRequest>()
};
var domain = new Uri(HttpContext.GetAppBaseUrl());
foreach (var route in group)
{
var constraint = route.ActionConstraints[0]?.Is<HttpMethodActionConstraint>();
var methodSummary = type.GetMethod(route.RouteValues["Action"]).GetSummary();
var methodDescriptor = route.Is<ControllerActionDescriptor>();
var request = new PostmanRequest
{
Url = new PostmanRequestUrl
{
Host = domain.Authority,
Path = route.AttributeRouteInfo.Template,
Protocol = HttpContext.Request.Scheme,
Query = new List<object>()
},
Method = constraint?.HttpMethods.FirstOrDefault() ?? "GET",
Headers = new List<PostmanHeader>
{
new PostmanHeader
{
Key = "Content-Type",
Value = "application/json"
}
},
Responses = new Collection<object>(),
Description = methodSummary,
};
var inputDictionary = methodDescriptor.Parameters.ToList()
.ToDictionary(parameter => parameter.Name, parameter => parameter.ParameterType.GetDefault());
request.Body = new PostmanBodyRequest
{
Mode = "raw",
Raw = inputDictionary.SerializeAsJson()
};
postManFolder.FolderRequests.Add(new PostmanFolderRequest
{
Name = route.RouteValues["Action"],
Request = request
});
}
postManCollection.Folders.Add(postManFolder);
}
return Json(postManCollection);
}
}
}
您还需要更新 PostmanRequestGet.cs 模型以使其正常工作。
更新如下:-
/// <summary>
/// the id of the collection that the request object belongs to
/// </summary>
[JsonProperty(PropertyName = "collectionId")]
public Guid CollectionId { get; set; }