惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Cisco Blogs
Schneier on Security
Schneier on Security
T
Tor Project blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Tenable Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
T
Threat Research - Cisco Blogs
C
CERT Recently Published Vulnerability Notes
Security Latest
Security Latest
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
NISL@THU
NISL@THU
L
Lohrmann on Cybersecurity
Scott Helme
Scott Helme
Webroot Blog
Webroot Blog
Project Zero
Project Zero
Google Online Security Blog
Google Online Security Blog
The Last Watchdog
The Last Watchdog
Spread Privacy
Spread Privacy
Hacker News: Ask HN
Hacker News: Ask HN
PCI Perspectives
PCI Perspectives
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
W
WeLiveSecurity
Attack and Defense Labs
Attack and Defense Labs
D
Darknet – Hacking Tools, Hacker News & Cyber Security
N
News | PayPal Newsroom
Help Net Security
Help Net Security
The Hacker News
The Hacker News
H
Heimdal Security Blog
O
OpenAI News
S
Security @ Cisco Blogs
N
News and Events Feed by Topic
Cyberwarzone
Cyberwarzone
Simon Willison's Weblog
Simon Willison's Weblog
G
GRAHAM CLULEY
www.infosecurity-magazine.com
www.infosecurity-magazine.com
博客园 - 叶小钗
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Hacker News - Newest:
Hacker News - Newest: "LLM"
T
Tailwind CSS Blog
大猫的无限游戏
大猫的无限游戏
A
Arctic Wolf
I
Intezer
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
Security Affairs
P
Proofpoint News Feed
S
Secure Thoughts
腾讯CDC
Google DeepMind News
Google DeepMind News
量子位
罗磊的独立博客

博客园 - 狂歌

关闭easyui Tabs,有意思的JS异步处理 vscode新版1.31.1使用代码检查工具ESlint支持VUE Easyui combotree 获取自定义ID属性方法 [原创]消灭eclipse中运行启动的错误:“找不到或无法加载主类”问题 [转]【Java】内部类(Inner Class)如何创建(new) spring boot项目编译出来的jar包如何更改端口号 [转] 分享一个快的飞起的maven的settings.xml文件 [转]maven打包报错:Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.5:test mysql字符集问题 错误代码: 1267 Illegal mix of collations (utf8_general_ci,IMPLICIT) and (utf8_croatian_ci,IMPLICIT) for operation '=' WebApi 返值的实体值前缀加了个下划线 VS2015 异常 :遇到异常。这可能是由某个扩展导致的 ASP.Net WebAPI中添加helppage帮助页面 C# List泛型转换,int,string 转字符,转数组 iBatisNet分布式事务的应用 MS SQL2008。 jquery.easyui.tabs 中的首个tabs被最后tabs覆盖的问题解决方法 解决easyui jQuery JS的for循环调用ajax异步问题 php网站环境无法上传的解决办法? 提高VM运行速度 [转]SQL2005系统升级手记之一-解决sa帐户被锁定
【转】WebAPI使用多个xml文件生成帮助文档
狂歌 · 2016-04-14 · via 博客园 - 狂歌

我们的注释就是帮助文档的说明或者说是描述,那么这个功能是安装了HelpPage就直接具有的吗,这里分两种方式。

1:创建项目时是直接选择的Web API,那么这时在创建初始化项目时就配置好此功能的。

2:创建项目时选择的是Empty,选择的核心引用选择Web API是不具有此功能。

对于方式1来说生成的项目代码有一部分我们是不需要的,我们可以做减法来删掉不必要的文件。

对于方式2来说,需要在Nuget内安装HelpPage,需要将文件~/Areas/HelpPage/HelpPageConfig.cs内的配置注释取消,具体的可以根据需要。

image

并且设置项目的生成属性内的输出,勾选Xml文档文件,同时设置值与~/Areas/HelpPage/HelpPageConfig.cs

内的配置一致。

image

并在Global.asax文件Application_Start方法注册。

AreaRegistration.RegisterAllAreas();

这时帮助文档已经可用,但却没有样式。你可以选择手动将需要的css及js拷入Areas文件夹内。并添加文件

public class BundleConfig
{
    // 有关绑定的详细信息,请访问 http://go.microsoft.com/fwlink/?LinkId=301862
    public static void RegisterBundles(BundleCollection bundles)
    {
        bundles.Add(new ScriptBundle('~/bundles/jquery').Include(
                    '~/Areas/HelpPage/Scripts/jquery-{version}.js'));

        // 使用要用于开发和学习的 Modernizr 的开发版本。然后,当你做好
        // 生产准备时,请使用 http://modernizr.com 上的生成工具来仅选择所需的测试。
        bundles.Add(new ScriptBundle('~/bundles/modernizr').Include(
                    '~/Areas/HelpPage/Scripts/modernizr-*'));

        bundles.Add(new ScriptBundle('~/bundles/bootstrap').Include(
                  '~/Areas/HelpPage/Scripts/bootstrap.js',
                  '~/Areas/HelpPage/Scripts/respond.js'));

        bundles.Add(new StyleBundle('~/Content/css').Include(
                  '~/Areas/HelpPage/Content/bootstrap.css',
                  '~/Areas/HelpPage/Content/site.css'));
    }
}

并在Global.asax文件Application_Start方法将其注册。

BundleConfig.RegisterBundles(BundleTable.Bundles);

最后更改~/Areas/HelpPage/Views/Shared/_Layout.cshtml 为

@using System
@using System.Web.Optimization
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv='Content-Type' content='text/html; charset=utf-8' />
    <meta charset='utf-8' />
    <meta name='viewport' content='width=device-width' />
    <title>@ViewBag.Title</title>
    @Styles.Render('~/Content/css')
    @Scripts.Render('~/bundles/modernizr')
</head>
<meta http-equiv='Content-Type' content='text/html; charset=utf-8' />
<body>
    <div class='navbar navbar-inverse navbar-fixed-top'>
        <div class='container'>
            <div class='navbar-header'>
                <button type='button' class='navbar-toggle' data-toggle='collapse' data-target='.navbar-collapse'>
                    <span class='icon-bar'></span>
                    <span class='icon-bar'></span>
                    <span class='icon-bar'></span>
                </button>
            </div>
            <div class='navbar-collapse collapse'>
                <ul class='nav navbar-nav'>
                    <li>@Html.Raw('<a href='/Help'>首页</a>')</li>
                    <li>@Html.Raw('<a href='/PostMan' target='_blank'>PostManFeture</a>')</li>
                </ul>
            </div>
        </div>
    </div>
    <div class='container body-content'>
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - 逗豆豆</p>
        </footer>
    </div>

    @Scripts.Render('~/bundles/jquery')
    @Scripts.Render('~/bundles/bootstrap')
    @RenderSection('scripts', required: false)
</body>
</html>

此时你看到的才会是如下的文档。

image

对应的路由结构如下

image

查看Route可以发现其 AllowMultiple = true 意味着我们可以对同一个Action定义多个不同的路由,但同时也意味着该Action只允许定义的路由访问。

比如这里的Get方法,这时在浏览器只能以这种方式访问 http://localhost:11488/api/product/{id}

image

用 http://localhost:11488/api/product?id={id} 则会抛出405,如下。

image

为了支持多种方式我们将路由增加,如下。

image

这时文档会将两种路由都生成出来。

image

这里有个原则是同类型的请求且响应的类型相同不允许定义相同的路由,如下,都是HttpGet 且响应类型相同。

/// <summary>
///     获取所有产品
/// </summary>
[HttpGet, Route('')]
public IEnumerable<Product> Get()
{
    return _products;
}
/// <summary>
///     获取前三产品
/// </summary>
[HttpGet, Route('')]
public IEnumerable<Product> GetTop3()
{
    return _products.Take(3);
}

此时访问 http://localhost:11488/api/product 会发现500错误,提示为匹配到多个Action,且这时候查看帮助文档也只会显示一个匹配的Action(前提是你没有指定Route的Order属性)。

image

路由内可以做一些基本的限制,我们将上面的Top3方法改造为可以根据传入参数来决定Top多少,并且最少是前三条。

/// <summary>
///     获取前几产品
/// </summary>
[HttpGet, Route('Top/{count:min(3)}')]
public IEnumerable<Product> GetTop(int count)
{
    return _products.Take(3);
}

这时访问 http://localhost:11488/api/product/Top/1 或 http://localhost:11488/api/product/Top/2 将会是抛出404

但是我希望直接访问 http://localhost:11488/api/product/Top 默认取前3条,这时直接访问会是405,因为并没有定义出Route(“Top”)的路由,我们改造下

/// <summary>
///     获取前几产品
/// </summary>
[HttpGet, Route('Top/{count:min(3):int=3}')]
public IEnumerable<Product> GetTop(int count)
{
    return _products.Take(3);
}

这时在访问 http://localhost:11488/api/product/Top 就会默认返回前3条了,除此之外还有一些定义包括正则可以 看这里 和 这里 。

路由的文档相关的基本就这些,有遗漏的地方欢迎指出。

接下来就是单个接口的Request和Response的文档,先来看看我们分别以Request和Response分开来看。

首先看下 api/Product/All 这个接口的显示,会发现分为两类。

image

api/Product 这个接口本身是就不需要任何参数的,因此都是None。

image

Put api/Product?id={id} 这接口确是都包含。他的定义如下。

/// <summary>
///     编辑产品
/// </summary>
/// <param name='id'>产品编号</param>
/// <param name='request'>编辑后的产品</param>
[HttpPut, Route(''), Route('{id}')]
public string Put(int id, Product request)
{
    var model = _products.FirstOrDefault(x => x.Id.Equals(id));
    if (model == null) return '未找到该产品';
    model.Name = request.Name;
    model.Price = request.Price;
    model.Description = request.Description;
    return 'ok';
}

那其实,实际中我们可能只会使用Get和Post来完成我们所有的操作。因此,就会是Get只显示URI Parameters 而 Post只显示Body Parameters。

可以看到Description就是我们对属性的注释,Type就是属性的类型,而Additional information 则是“约束”的描述,如我们会约束请求的参数哪些为必填哪些为选填,哪些参数的值具有使用范围。

比如我们改造一下Product。

/// <summary>
///     产品
/// </summary>
public class Product
{
    /// <summary>
    ///     编号
    /// </summary>
    [Required]
    public int Id { get; set; }
    /// <summary>
    ///     名称
    /// </summary>
    [Required, MaxLength(36)]
    public string Name { get; set; }
    /// <summary>
    ///     价格
    /// </summary>
    [Required, Range(0, 99999999)]
    public decimal Price { get; set; }
    /// <summary>
    ///     描述
    /// </summary>
    public string Description { get; set; }
}

可以看见对应的“约束信息”就改变了。

image

有人可能会说,我自定义了一些约束该怎么显示呢,接下来我们定义一个最小值约束MinAttrbute。

/// <summary>
///     最小值特性
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class MinAttribute : ValidationAttribute
{
    /// <summary>
    ///     最小值
    /// </summary>
    public int MinimumValue { get; set; }

    /// <summary>
    ///     构造函数
    /// </summary>
    /// <param name='minimun'></param>
    public MinAttribute(int minimun)
    {
        MinimumValue = minimun;
    }

    /// <summary>
    ///     验证逻辑
    /// </summary>
    /// <param name='value'>需验证的值</param>
    /// <returns>是否通过验证</returns>
    public override bool IsValid(object value)
    {
        int intValue;
        if (value != null && int.TryParse(value.ToString(), out intValue))
        {
            return (intValue >= MinimumValue);
        }
        return false;
    }

    /// <summary>
    ///     格式化错误信息
    /// </summary>
    /// <param name='name'>属性名称</param>
    /// <returns>错误信息</returns>
    public override string FormatErrorMessage(string name)
    {
        return string.Format('{0} 最小值为 {1}', name, MinimumValue);
    }
}

将其加在Price属性上,并将最小值设定为10。

/// <summary>
///     价格
/// </summary>
[Required, Min(10)]
public decimal Price { get; set; }

这时通过PostMan去请求,会发现验证是通过的,并没有预计的错误提示。那是因为我们没有启用验证属性的特性。

我们自定义一个ValidateModelAttribute,可用范围指定为Class和Method,且不允许多次,并将其加到刚才的Put接口上。

/// <summary>
///     验证模型过滤器
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ValidateModelAttribute : ActionFilterAttribute
{
    /// <summary>
    ///     Action执行前验证
    /// </summary>
    /// <param name='actionContext'>The action context.</param>
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (actionContext.ActionArguments.Any(kv => kv.Value == null))
        {
            actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, '参数不能为空');
        }
        if (actionContext.ModelState.IsValid) return;
        actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
    }
}
/// <summary>
///     编辑产品
/// </summary>
/// <param name='id'>产品编号</param>
/// <param name='request'>编辑后的产品</param>
[HttpPut, Route(''), Route('{id}')]
[ValidateModel]
public string Put(int id, Product request)
{
    var model = _products.FirstOrDefault(x => x.Id.Equals(id));
    if (model == null) return '未找到该产品';
    model.Name = request.Name;
    model.Price = request.Price;
    model.Description = request.Description;
    return 'ok';
}

这是我们使用PostMan请求,验证提示便出现了。

image

但这时候看我们的帮助文档,Price的“约束信息”就仅剩Required一个了。

image

那我要将自定义的MinAttribute的约束信息也显示出来该怎么办呢,观察文档的生成代码可以发现是在Areas.HelpPage.ModelDescriptions.ModelDescriptionGenerator类中的AnnotationTextGenerator内的定义生成的。

那既然如此就好办了,我将我自定义的也加进去。

// Modify this to support more data annotation attributes.
private readonly IDictionary<Type, Func<object, string>> AnnotationTextGenerator = new Dictionary<Type, Func<object, string>>
{
    { typeof(RequiredAttribute), a => 'Required' },
    { typeof(RangeAttribute), a =>
        {
            RangeAttribute range = (RangeAttribute)a;
            return String.Format(CultureInfo.CurrentCulture, 'Range: inclusive between {0} and {1}', range.Minimum, range.Maximum);
        }
    },
    { typeof(MaxLengthAttribute), a =>
        {
            MaxLengthAttribute maxLength = (MaxLengthAttribute)a;
            return String.Format(CultureInfo.CurrentCulture, 'Max length: {0}', maxLength.Length);
        }
    },
    { typeof(MinLengthAttribute), a =>
        {
            MinLengthAttribute minLength = (MinLengthAttribute)a;
            return String.Format(CultureInfo.CurrentCulture, 'Min length: {0}', minLength.Length);
        }
    },
    { typeof(StringLengthAttribute), a =>
        {
            StringLengthAttribute strLength = (StringLengthAttribute)a;
            return String.Format(CultureInfo.CurrentCulture, 'String length: inclusive between {0} and {1}', strLength.MinimumLength, strLength.MaximumLength);
        }
    },
    { typeof(DataTypeAttribute), a =>
        {
            DataTypeAttribute dataType = (DataTypeAttribute)a;
            return String.Format(CultureInfo.CurrentCulture, 'Data type: {0}', dataType.CustomDataType ?? dataType.DataType.ToString());
        }
    },
    { typeof(RegularExpressionAttribute), a =>
        {
            RegularExpressionAttribute regularExpression = (RegularExpressionAttribute)a;
            return String.Format(CultureInfo.CurrentCulture, 'Matching regular expression pattern: {0}', regularExpression.Pattern);
        }
    },
    { typeof(MinAttribute), a =>
        {
            MinAttribute minAttribute = (MinAttribute)a;
            return String.Format(CultureInfo.CurrentCulture, '最小值: {0}', minAttribute.MinimumValue);
        }
    },
};

接着再看文档,我们的“约束信息”就出来了。

image

Request部分基本也就这些了。Response部分没太多内容,主要就是Sample的显示会有一个问题,你若是一步一步写到这里看到的帮助文档Sample会有三个,分别是

application/json,text/json  application/xml,text/xml   application/x-www-from-urlencoded

image

这里我们会发现它生成不了 application/x-www-form-urlencoded,是因为无法使用JqueryMvcFormUrlEncodeFomatter来格式我们的类。至于为什么,我没有去找,因为除了application/json是我需要的之外其余的我都不需要。

有兴趣的朋友可以找找为什么。然后告知一下~那这里我们将不需要的移除,如下。

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API 配置和服务
        config.Formatters.Remove(config.Formatters.XmlFormatter);

        // Web API 路由
        config.MapHttpAttributeRoutes();
    }
}

这里只移除了XmlFormatter,因为application/x-www-form-urlencoded我们在请求的时候还需要,但我不想让他显示在文档中,于是…

在Areas.HelpPage.SampleGeneration.HelpPageSampleGenerator类中的 GetSample 方法内将

foreach (var formatter in formatters)

更改为

foreach (var formatter in formatters.Where(x => x.GetType() != typeof(JQueryMvcFormUrlEncodedFormatter)))

然后,文档就干净了,这难道是洁癖么…

image

终于到这了,我们首先将Product单独作为一个项目 WebAPI2PostMan.WebModel 并引用他,查看文档如下。

image

你会发现,你的注释也就是属性的描述没有了。打开App_Data/XmlDocument.xml文件对比之前P没移动roduct的xml文件确实Product类的描述确实没有了,因为此处的XmlDocument.xml文件是项目的生成描述文件,不在此项目

内定义的文件是不会生成在这个文件内的,那真实的需求是我们确确实实需要将所有Request和Response单独定义在一个项目内供其它项目引用,可能是单元测试也可能是我们封装的WebAPI客户端(此处下篇文章介绍)。

带着这个疑问找到了这样一篇文章 http://stackoverflow.com/questions/21895257/how-can-xml-documentation-for-web-api-include-documentation-from-beyond-the-main

该文章提供了3种办法,这里只介绍我认为合理的方法,那那就是我们就需要将 WebAPI2PostMan.WebModel 的生成属性也勾选XML文档文件,就是也生成一个xml文档,同时拓展出一个新的Xml文档加载方式

在目录 ~/Areas/HelpPage/ 下新增一个名为 MultiXmlDocumentationProvider.cs 的类。

using System;
using System.Linq;
using System.Reflection;
using System.Web.Http.Controllers;
using System.Web.Http.Description;
using Xlobo.RechargeService.Areas.HelpPage.ModelDescriptions;

namespace Xlobo.RechargeService.Areas.HelpPage
{
    /// <summary>A custom <see cref='IDocumentationProvider'/> that reads the API documentation from a collection of XML documentation files.</summary>
    public class MultiXmlDocumentationProvider : IDocumentationProvider, IModelDocumentationProvider
    {
        /*********
        ** Properties
        *********/
        /// <summary>The internal documentation providers for specific files.</summary>
        private readonly XmlDocumentationProvider[] Providers;


        /*********
        ** Public methods
        *********/
        /// <summary>Construct an instance.</summary>
        /// <param name='paths'>The physical paths to the XML documents.</param>
        public MultiXmlDocumentationProvider(params string[] paths)
        {
            this.Providers = paths.Select(p => new XmlDocumentationProvider(p)).ToArray();
        }

        /// <summary>Gets the documentation for a subject.</summary>
        /// <param name='subject'>The subject to document.</param>
        public string GetDocumentation(MemberInfo subject)
        {
            return this.GetFirstMatch(p => p.GetDocumentation(subject));
        }

        /// <summary>Gets the documentation for a subject.</summary>
        /// <param name='subject'>The subject to document.</param>
        public string GetDocumentation(Type subject)
        {
            return this.GetFirstMatch(p => p.GetDocumentation(subject));
        }

        /// <summary>Gets the documentation for a subject.</summary>
        /// <param name='subject'>The subject to document.</param>
        public string GetDocumentation(HttpControllerDescriptor subject)
        {
            return this.GetFirstMatch(p => p.GetDocumentation(subject));
        }

        /// <summary>Gets the documentation for a subject.</summary>
        /// <param name='subject'>The subject to document.</param>
        public string GetDocumentation(HttpActionDescriptor subject)
        {
            return this.GetFirstMatch(p => p.GetDocumentation(subject));
        }

        /// <summary>Gets the documentation for a subject.</summary>
        /// <param name='subject'>The subject to document.</param>
        public string GetDocumentation(HttpParameterDescriptor subject)
        {
            return this.GetFirstMatch(p => p.GetDocumentation(subject));
        }

        /// <summary>Gets the documentation for a subject.</summary>
        /// <param name='subject'>The subject to document.</param>
        public string GetResponseDocumentation(HttpActionDescriptor subject)
        {
            return this.GetFirstMatch(p => p.GetDocumentation(subject));
        }


        /*********
        ** Private methods
        *********/
        /// <summary>Get the first valid result from the collection of XML documentation providers.</summary>
        /// <param name='expr'>The method to invoke.</param>
        private string GetFirstMatch(Func<XmlDocumentationProvider, string> expr)
        {
            return this.Providers
                .Select(expr)
                .FirstOrDefault(p => !String.IsNullOrWhiteSpace(p));
        }
    }
}

接着替换掉原始 ~/Areas/HelpPage/HelpPageConfig.cs 内的配置。

//config.SetDocumentationProvider(new XmlDocumentationProvider(HttpContext.Current.Server.MapPath('~/App_Data/XmlDocument.xml')));
config.SetDocumentationProvider(new MultiXmlDocumentationProvider(HttpContext.Current.Server.MapPath('~/App_Data/XmlDocument.xml'), HttpContext.Current.Server.MapPath('~/App_Data/WebAPI2PostMan.WebModel.XmlDocument.xml')));

那这里你可以选择多个文档xml放置于不同位置也可以采用将其都放置于WebAPI项目下的App_Data下。

为了方便我们在WebAPI项目下,这里指 WebAPI2PostMan,对其添加生成事件

copy $(SolutionDir)WebAPI2PostMan.WebModelApp_DataXmlDocument.xml $(ProjectDir)App_DataWebAPI2PostMan.WebModel.XmlDocument.xml

每次生成成功后将 WebAPI2PostMan.WebModel.XmlDocument.xml 文件拷贝到 WebAPI2PostMan项目的App_Data目录下,并更名为 WebAPI2PostMan.WebModel.XmlDocument.xml。

至此,重新生成项目,我们的描述就又回来了~

这篇文章若耐心看完会发现其实就改动几处而已,没必要花这么大篇幅来说明,但是对需要的人来说还是有一点帮助的。

为了方便,源代码依然在:https://github.com/yanghongjie/WebAPI2PostMan ,若你都看到这里了,顺手点下【推荐】吧(●'?'●)。