【轉】WebAPI使用多個xml文件生成幫助文檔

來自:http://www.it165.net/pro/html/201505/42504.html

1、前言

上篇有提到在WebAPI項目內,經過在Nuget裏安裝(Microsoft.AspNet.WebApi.HelpPage)能夠根據註釋生成幫助文檔,查看代碼實現會發現是基於解析項目生成的xml文檔來做爲數據源從而展現出來的。在咱們的項目幫助文檔須要的類(特指定義的Request和Response)與項目在同一個項目時是沒有問題的,可是咱們實際工做中會由於其餘項目也須要引用該(Request和Response)時,咱們會將其抽出來單獨做爲一個項目供其它調用來引用,這時,查看幫助文檔不會報錯,可是註釋以及附加信息將會丟失,由於這些信息是咱們的代碼註釋和數據註釋(如 [Required]標識爲必填),也是生成到xml文檔中的信息,但因不在同一項目內,將讀取不到從而致使幫助文檔沒法顯示咱們的註釋(對應的描述)和附加信息(是否必填、默認值、Range等).css

2、幫助文檔註釋概要

咱們的註釋就是幫助文檔的說明或者說是描述,那麼這個功能是安裝了HelpPage就直接具備的嗎,這裏分兩種方式。html

1:建立項目時是直接選擇的Web API,那麼這時在建立初始化項目時就配置好此功能的。jquery

2:建立項目時選擇的是Empty,選擇的核心引用選擇Web API是不具備此功能。git

對於方式1來講生成的項目代碼有一部分咱們是不須要的,咱們能夠作減法來刪掉沒必要要的文件。github

對於方式2來講,須要在Nuget內安裝HelpPage,須要將文件~/Areas/HelpPage/HelpPageConfig.cs內的配置註釋取消,具體的能夠根據須要。web

image

而且設置項目的生成屬性內的輸出,勾選Xml文檔文件,同時設置值與~/Areas/HelpPage/HelpPageConfig.csexpress

內的配置一致。json

image

並在Global.asax文件Application_Start方法註冊。bootstrap

AreaRegistration.RegisterAllAreas();

這時幫助文檔已經可用,但卻沒有樣式。你能夠選擇手動將須要的css及js拷入Areas文件夾內。並添加文件c#

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

3、使用多個項目生成Xml文件來顯示幫助文檔

終於到這了,咱們首先將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 ,若你都看到這裏了,順手點下【推薦】吧(●'?'●)。

相關文章
相關標籤/搜索