在开发 ASP.NET WebApi 时,经常需要自测接口或供前端临时调试。Postman、Fiddler 固然强大,但来回切换环境、拼装请求也挺烦的。WebApiTestClient 这个 NuGet 包可以直接把测试页面嵌入项目,通过浏览器就能调,不用额外安装任何东西。
它的好处总结下来就几点:零依赖集成在项目里;支持自定义 HTTP 方法、请求头和请求体;响应状态码、响应头、响应体即时显示;前端同学也能直接打开 Help Page 去调接口,联调成本低一些。
不过要让它工作,配置过程有几个坑点,这里记录一下。
1. 安装包
在 VS 里打开 WebApi 项目,NuGet 搜索 WebApiTestClient 安装就行。装完之后,项目里会多出这几个文件:
Scripts\WebApiTestClient.jsAreas\HelpPage\TestClient.cssAreas\HelpPage\Views\Help\DisplayTemplates\TestClientDialogs.cshtmlAreas\HelpPage\Views\Help\DisplayTemplates\TestClientReferences.cshtml
2. 修改帮助页面视图
找到 Areas\HelpPage\Views\Help\Api.cshtml,在末尾把测试对话框和引用模板渲染出来。我的做法是在原有内容下面加上:
@Html.DisplayForModel("TestClientDialogs")
@section Scripts{
<link href="~/Areas/HelpPage/HelpPage.css" rel="stylesheet" />
@Html.DisplayForModel("TestClientReferences")
}
如果你项目里的 Api.cshtml 本身就有 @section Scripts 块,直接把这两行合并进去就行。
原来的文件大概长这样(不同模板可能有差别):
@using System.Web.Http
@using WebApiDemo.Areas.HelpPage.Models
@model HelpPageApiModel
@{ var description = Model.ApiDescription; ViewBag.Title = description.HttpMethod.Method + " " + description.RelativePath; }
<link type="text/css" href="~/Areas/HelpPage/HelpPage.css" rel="stylesheet" />
<div>
<section>
<div> <p> @Html.ActionLink("Help Page Home", "Index") </p> </div> </section>
<section> @Html.DisplayForModel() </section> </div>
加上测试相关部分后,页面底部就会多出测试按钮。
3. 配好 XML 文档注释
想让接口列表自动显示你在代码里写的 /// <summary> 注释,需要把 XML 文档生成打开。右键项目 → 属性 → 生成 → 勾选'XML 文档文件',路径默认是 App_Data\项目名.xml。
然后打开 Areas/HelpPage/HelpPageConfig.cs,在 Register 方法里把那个被注释掉的 SetDocumentationProvider 打开,并把路径改成你自己的:
config.SetDocumentationProvider(
new XmlDocumentationProvider(
HttpContext.Current.Server.MapPath("~/App_Data/WebApiDemo.xml")
)
);
原文件里有很多注释,大部分是示例,不动也没事。关键是确保上面这行是生效的。完整的 HelpPageConfig.cs 可以参考:
// Uncomment the following to provide samples for PageResult<T>. Must also add the Microsoft.AspNet.WebApi.OData // package to your project. ////#define Handle_PageResultOfT using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net.Http.Headers; using System.Reflection; using System.Web; using System.Web.Http; #if Handle_PageResultOfT using System.Web.Http.OData; #endif namespace WebApiDemo.Areas.HelpPage { /// <summary> /// Use this class to customize the Help Page. /// For example you can set a custom <see cref="System.Web.Http.Description.IDocumentationProvider"/> to supply the documentation /// or you can provide the samples for the requests/responses. /// </summary> public static class HelpPageConfig { [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "WebApiDemo.Areas.HelpPage.TextSample.#ctor(System.String)", Justification = "End users may choose to merge this string with existing localized resources.")] [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "bsonspec", Justification = "Part of a URI.")] public static void Register(HttpConfiguration config) { //// Uncomment the following to use the documentation from XML documentation file. //config.SetDocumentationProvider(new XmlDocumentationProvider(HttpContext.Current.Server.MapPath("~/App_Data/XmlDocument.xml"))); //// Uncomment the following to use "sample string" as the sample for all actions that have string as the body parameter or return type. //// Also, the string arrays will be used for IEnumerable<string>. The sample objects will be serialized into different media type //// formats by the available formatters. //config.SetSampleObjects(new Dictionary<Type, object> //{ // {typeof(string), "sample string"}, // {typeof(IEnumerable<string>), new string[]{"sample 1", "sample 2"}} //}); // Extend the following to provide factories for types not handled automatically (those lacking parameterless // constructors) or for which you prefer to use non-default property values. Line below provides a fallback // since automatic handling will fail and GeneratePageResult handles only a single type. #if Handle_PageResultOfT config.GetHelpPageSampleGenerator().SampleObjectFactories.Add(GeneratePageResult); #endif // Extend the following to use a preset object directly as the sample for all actions that support a media // type, regardless of the body parameter or return type. The lines below avoid display of binary content. // The BsonMediaTypeFormatter (if available) is not used to serialize the TextSample object. config.SetSampleForMediaType( new TextSample("Binary JSON content. See http://bsonspec.org for details."), new MediaTypeHeaderValue("application/bson")); //// Uncomment the following to use "[0]=foo&[1]=bar" directly as the sample for all actions that support form URL encoded format //// and have IEnumerable<string> as the body parameter or return type. //config.SetSampleForType("[0]=foo&[1]=bar", new MediaTypeHeaderValue("application/x-www-form-urlencoded"), typeof(IEnumerable<string>)); //// Uncomment the following to use "1234" directly as the request sample for media type "text/plain" on the controller named "Values" //// and action named "Put". //config.SetSampleRequest("1234", new MediaTypeHeaderValue("text/plain"), "Values", "Put"); //// Uncomment the following to use the image on "../images/aspNetHome.png" directly as the response sample for media type "image/png" //// on the controller named "Values" and action named "Get" with parameter "id". //config.SetResponse(new ImageSample("../images/aspNetHome.png"), new MediaTypeHeaderValue("image/png"), "Values", "Get", "id"); //// Uncomment the following to correct the sample request when the action expects an HttpRequestMessage with ObjectContent<string>. //// The sample will be generated as if the controller named "Values" and action named "Get" were having string as the body parameter. //config.SetActualRequestType(typeof(string), "Values", "Get"); //// Uncomment the following to correct the sample response when the action returns an HttpResponseMessage with ObjectContent<string>. //// The sample will be generated as if the controller named "Values" and action named "Post" were returning a string. //config.SetActualResponseType(typeof(string), "Values", "Post"); config.SetDocumentationProvider(new XmlDocumentationProvider(HttpContext.Current.Server.MapPath("~/App_Data/WebApiDemo.xml"))); } #if Handle_PageResultOfT private static object GeneratePageResult(HelpPageSampleGenerator sampleGenerator, Type type) { if (type.IsGenericType) { Type openGenericType = type.GetGenericTypeDefinition(); if (openGenericType == typeof(PageResult<>)) { // Get the T in PageResult<T> Type[] typeParameters = type.GetGenericArguments(); Debug.Assert(typeParameters.Length == 1); // Create an enumeration to pass as the first parameter to the PageResult<T> constuctor Type itemsType = typeof(List<>).MakeGenericType(typeParameters); object items = sampleGenerator.GetSampleObject(itemsType); // Fill in the other information needed to invoke the PageResult<T> constuctor Type[] parameterTypes = new Type[] { itemsType, typeof(Uri), typeof(long?), }; object[] parameters = new object[] { items, null, (long)ObjectGenerator.DefaultCollectionSize, }; // Call PageResult(IEnumerable<T> items, Uri nextPageLink, long? count) constructor ConstructorInfo constructor = type.GetConstructor(parameterTypes); return constructor.Invoke(parameters); } } return null; } #endif } }







