65.9K
CodeProject 正在变化。 阅读更多。
Home

Web API 想法 3/3 - 扩展 Web API 文档

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (114投票s)

2014年11月4日

CPOL

5分钟阅读

viewsIcon

72704

downloadIcon

640

与 ASP.NET Web API 相关的项目。通过关于数据流、使用 HTTPS 和扩展 Web API 文档的文章,涉及该技术的大部分方面。

这是关于该主题的延续部分。如需回顾,请参阅 WebAPI Thoughts - Introduction 文章。

 PM > Install-Package WebAPI-DocumentX

扩展 Web API 文档

Web API 最酷的另一个特性是它能够记录 Web API 服务方法。该功能通过列出、描述服务方法并为其对应的请求/响应值生成示例来帮助记录服务方法。尽管现有功能提供了良好的 API 文档,但它无法记录某些请求/响应数据类型,也没有告知用户文档的确切情况。

问题

假设您定义了一个名为 AddInternalPhysician 的服务方法,它接受 Physician 类类型。但是这个 Physician 类是一个 abstract 类。然后,您尝试通过 API 文档帮助部分查看该方法的文档,但即使该方法已在您的 ApiContoller 中定义,您也看不到它。您甚至不知道该方法 API 文档发生了什么。对于返回 HttpResponseMessage 对象的方法以及接受 HttpRequestMessage 对象的方法也是如此。

Visual Studio 2012 更新 1 (SP1) 试图通过添加一个 ResponseType 属性(位于 System.Web.Http.Description 命名空间)来解决此问题,该属性将用于定义自定义响应类型。尽管如此,由于我之前已解释过的各种原因,问题仍然存在。

解决方案

一种可能的解决方案是扩展 API 文档提供程序,以便 Web API 方法将使用实际的请求/响应类进行标记。这个概念与 VS 团队尝试过的类似,但更通用,并且可以解决上述问题。

该库将记录具有以下特征的 Web API 方法:

  • 多个请求参数。
  • 请求/响应类型的抽象/接口。
  • 请求/响应类型的 HttpRequestMessageHttpResponseMessage
  • 根据服务方法逻辑,返回 SuccessResponseType ErrorResponseType 的响应。
  • 提供关于方法请求/响应类型情况的解释。

参与的项目

  • WebAPICommonLibrary
  • WebAPIDocumentationDemo
  • WebAPIDocumentationExtenderLibrary
  • POCOLibrary

如何使用

  1. WebAPIDocumentationExtenderLibrary 添加/引用到您的 Web API 项目
  2. Global.asax.csWebApiConfig.cs 下引用该库
    using WebAPIDocumentationExtenderLibrary;
  3. 对于 Global.asax.cs
    protected void Application_Start()
    {
        // .........................................
        // Full code is available in the source code
        // .........................................
        GlobalConfiguration.Configuration.RegisterRequestResponseHelp();
    }
        

    对于 WebApiConfig.cs

    public static class WebApiConfig
    {
       public static void Register(HttpConfiguration config)
       {
          // .........................................
          // Full code is available in the source code
          // .........................................
    
          config.RegisterRequestResponseHelp();
       }
    }
        
  4. 一旦将库引用(步骤 2)到所需的 خدمة控制器类,就向操作添加 RequestTypeAttributeResponseTypeAttribute,并指定相应的请求/响应类型。例如
    [RequestType(typeof(InternalPhysicianBase), "physicianRequest")]
    [ResponseType(typeof(bool), typeof(Message))] // for success return true/false, for error/exception return message type with appropriate content. 
    [HttpPost]
    public HttpResponseMessage AddInternalPhysician(HttpRequestMessage physicianRequest)
    {
         InternalPhysician internalPhysician = physicianRequest.Content.ReadAsAsync<InternalPhysician>().Result;
         return base.AddPhysician(internalPhysician);
    }
    

以下是 AddInternalPhysician 方法文档的输出示例

Web API 文档扩展库的工作原理

该解决方案背后的基本思想是通过反射来检测 Web API 项目程序集,然后为 Web API 项目中定义的每个方法构建示例,最后将自定义消息和示例注册到 Web API 文档部分。该库由许多类和接口组成,以促进解决方案的实现。它还通过 .NET 反射使用现有的 Web API 文档类来生成所需的文档示例,并将 Web API 方法注册到适当的文档位置。这些类/接口显示在以下类图

从图中可以看出,有三个核心接口促进了文档示例的生成。这些接口定义如下

/// <summary>
/// API Documentation Builder interface
/// </summary>
internal interface IFluentBuilder
{
    /// <summary>
    /// Get Sample
    /// </summary>
    string Sample { get; }

    /// <summary>
    /// Build sample API Documentation
    /// </summary>
    /// <param name="input">Input value</param>
    /// <returns>IFluentBuilder object</returns>
    IFluentBuilder BuildSample(string input);

}

/// <summary>
/// Request sample builder interface
/// </summary>
internal interface IFluentRequestBuilder : IFluentBuilder
{
    /// <summary>
    /// Build request sample API Documentation
    /// </summary>
    /// <param name="type">Type value</param>
    /// <param name="parameterName">ParameterName value</param>
    /// <returns>IFluentRequestBuilder object</returns>
    IFluentRequestBuilder BuildSample(Type type, string parameterName);
}

/// <summary>
/// Response sample builder interface
/// </summary>
internal interface IFluentResponseBuilder : IFluentBuilder
{
    /// <summary>
    /// Build response sample API Documentation
    /// </summary>
    /// <param name="type">Type value</param>
    /// <returns>IFluentResponseBuilder object</returns>
    IFluentResponseBuilder BuildSample(Type type);

    /// <summary>
    /// Build response sample API Documentation
    /// </summary>
    /// <param name="successResponseType">Success response type</param>
    /// <param name="errorResponseType">Error response type</param>
    /// <returns>IFluentResponseBuilder object</returns>
    IFluentResponseBuilder BuildSample(Type successResponseType, Type errorResponseType);
}

名为 APISampleBuilder 的基本 abstract 类实现了 IFluentRequestBuilderIFluentResponseBuilder 这两个接口,用于为方法的请求和响应构建所需的示例输出。它还包含用于促进示例输出的 private 方法。定义了两个派生类 JSONSampleBuilderXMLSampleBuilder,分别用于促进 JSON 和 XML 示例输出。这些类中的每一个都实现了一个名为 BuildSampleabstract 方法,该方法接受一个对象实例作为输入。

/// <summary>
/// API Documentation builder abstract class
/// </summary>
internal abstract class APISampleBuilder : IFluentRequestBuilder, IFluentResponseBuilder
{   
    // --------------------------------------------
    // Full Code is available in the source control
    // --------------------------------------------

    /// <summary>
    /// Build sample API Documentation
    /// </summary>
    /// <param name="instance">Instance value</param>
    /// <returns>IFluentBuilder object</returns>
    public abstract IFluentBuilder BuildSample(object instance);

    /// <summary>
    /// Build sample API Documentation
    /// </summary>
    /// <param name="input">Input value</param>
    /// <returns>IFluentBuilder object</returns>
    public IFluentBuilder BuildSample(string input)
    {
        if (!string.IsNullOrWhiteSpace(input))
            sampleStringBuilder.AppendLine(input);
        return this;
    }

    /// <summary>
    /// Build request sample
    /// </summary>
    /// <param name="type">Type value</param>
    /// <param name="parameterName">ParameterName value</param>
    /// <returns>IFluentRequestBuilder object</returns>
    public IFluentRequestBuilder BuildSample(Type type, string parameterName)
    {
        string header = "Request";
        string messageHeader = string.Empty;
        if (!string.IsNullOrWhiteSpace(parameterName))
        {
            messageHeader = string.Format("{0} sample for {1} ", header, parameterName);
            BuildSample(messageHeader)
                .BuildSample(messageLiner);
        }
        else
            BuildSample(messageLiner);

        return BuilderSample(type, header) as IFluentRequestBuilder;
    }

    /// <summary>
    /// Build response sample
    /// </summary>
    /// <param name="type">Type value</param>
    /// <returns>IFluentResponseBuilder object</returns>
    public IFluentResponseBuilder BuildSample(Type type)
    {
        string header = "Response";
        BuildSample(messageLiner);
        return BuilderSample(type, header) as IFluentResponseBuilder;
    }   
   
    /// <summary>
    /// Build response sample API Documentation
    /// </summary>
    /// <param name="successResponseType">Success response type</param>
    /// <param name="errorResponseType">Error response type</param>
    /// <returns>IFluentResponseBuilder object</returns>
    public IFluentResponseBuilder BuildSample(Type successResponseType, Type errorResponseType)
    {
      return ((BuildSample("Success response message sample") as IFluentResponseBuilder)
              .BuildSample(successResponseType)
              .BuildSample("Error response message sample") as IFluentResponseBuilder)
              .BuildSample(errorResponseType);
    }   
}

/// <summary>
/// JSON sample builder class
/// </summary>
sealed class JSONSampleBuilder : APISampleBuilder
{
    // --------------------------------------------
    // Full code is available in the source control
    // --------------------------------------------

    /// <summary>
    /// BuildSample API Documentation sample
    /// </summary>
    /// <param name="instance">Instance value</param>
    /// <returns>IFluentBuilder object</returns>
    public override IFluentBuilder BuildSample(object instance)
    {
        string json = string.Empty;
        try
        {
            // Helps to serialzied the exact type of the object. i.e Base vs Derived classes 
            JsonSerializerSettings jss = new JsonSerializerSettings();

            if (_jsonFormatter != null && _jsonFormatter.SerializerSettings != null &&
                _jsonFormatter.SerializerSettings.TypeNameHandling != TypeNameHandling.None)
            {
                jss = _jsonFormatter.SerializerSettings;
            }
            else
            {
                jss.TypeNameHandling = TypeNameHandling.Auto;
            };
            json = JsonConvert.SerializeObject(instance, Formatting.Indented, jss);
        }
        catch (Exception)
        {
        }
        return base.BuildSample(json);
    }
}

/// <summary>
/// XML Sample builder class
/// </summary>
sealed class XMLSampleBuilder : APISampleBuilder
{
    // --------------------------------------------
    // Full code is available in the source control
    // --------------------------------------------

    /// <summary>
    /// BuildSample API Documentation sample
    /// </summary>
    /// <param name="instance">Instance value</param>
    /// <returns>IFluentBuilder object</returns>
    public override IFluentBuilder BuildSample(object instance)
    {
        string xml = string.Empty;
        try
        {
            using (Stream streamWriter = new MemoryStream())
            using (StreamReader streamReader = new StreamReader(streamWriter))
            {
                DataContractSerializer xmlSerializer = new DataContractSerializer(instance.GetType());
                xmlSerializer.WriteObject(streamWriter, instance);
                streamWriter.Position = 0;
                xml = streamReader.ReadToEnd();
                xml = XElement.Parse(xml).ToString(); // Helps for proper indentation
            }
        }
        catch (Exception)
        {
        }
        return base.BuildSample(xml);
    }
}

RequestTypeAttributeResponseTypeAttibute 将帮助装饰 Web API 控制器方法的所需请求/响应类型。RequestTypeAttribute 通过其构造函数接受两个参数。一个用于要记录的实际类型,另一个用于显示类型是定义在操作参数中的哪个参数。

/// <summary>
/// RequestType attribute class used to decorate Web API request objects
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class RequestTypeAttribute : Attribute
{
    // --------------------------------------------
    // Full code is available in the source control
    // --------------------------------------------

    /// <summary>
    /// RequestType attribute class used to decorate Web API request objects
    /// </summary>
    /// <param name="type">Type value that represents the any request value</param>
    /// <param name="parameterName">ParameterName value that represents the request value</param>
    public RequestTypeAttribute(Type type, string parameterName)
    {
        if (type == null)
            throw new ArgumentNullException("Request type value is null !");

        Type = type;
        _parameterName = parameterName;
    }
}

ResponseTypeAttribute 有两个重载的构造函数定义,用于

  1. 通用响应对象
  2. 与第一个类似,除了用户需要传递两个类型,即 SuccessResponseErrorResponse。分别代表成功响应和错误响应。
/// <summary>
/// Response attribute class used to decorate Web API response objects
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class ResponseTypeAttribute : Attribute
{
    // --------------------------------------------
    // Full Code is available in the source control
    // --------------------------------------------

    /// <summary>
    /// ResponseType attribute class used to decorate Web API response objects
    /// </summary>
    /// <param name="type">Type value that represents the any response object</param>
    public ResponseTypeAttribute(Type type)
    {
        if (type == null)
            throw new ArgumentNullException("Response type value is null !");

        Type = type;
    }

    /// <summary>
    /// ResponseType attribute class used to decorate Web API response objects
    /// </summary>
    /// <param name="successResponseType">
    /// Type value that represents success response</param>
    /// <param name="errorResponseType"> Type value that represents error response </param>
    public ResponseTypeAttribute(Type successResponseType, Type errorResponseType)
    {
        if (successResponseType == null)
            throw new ArgumentNullException("Success Response type value is null !");
        
        if (errorResponseType == null)
            throw new ArgumentNullException("Error Response type value is null !");

        SuccessResponseType = successResponseType;

        ErrorResponseType = errorResponseType;
    }
}

为清晰起见,请参阅 如何使用 部分。

一旦定义了所有这些类,示例文档将按以下方式生成

  1. 加载 Web API 项目程序集
  2. 验证程序集
    • 它应该至少包含一个 ApiController 类型。
    • 它必须包含一个 HelpPageConfigurationExtensions 类。
    • 它必须包含一个 ObjectGenerator 类。
    • HelpPageConfigurationExtensions 类应该定义带有正确输入参数的 SetSampleRequestSetSampleResponse 方法。
    • ObjectGenerator 类应该定义带有正确输入参数的 GenerateObject 方法。

  3. 收集适用于请求和响应 API 文档的方法。
  4. ApiController 过滤掉任何重复的请求和响应方法。
  5. 最后,将这些方法注册以生成 API 文档的示例。

因此,为了执行上述过程,三个类参与其中,即 RegisterRequestTypesRegisterResponseTypesRegisterAPIHelp。顾名思义,前两个负责请求和响应示例文档,最后一个负责验证和注册请求/响应示例 API 文档。执行其职责的操作显示在以下代码中

/// <summary> 
/// Register Request Type class
/// </summary>
internal sealed class RegisterRequestTypes
{
    /// <summary>
    /// Register Request types 
    /// </summary>
    /// <param name="httpConfiguration">HttpConfiguration value</param>
    /// <param name="setSampleRequest">SampleRequest MethodInfo value</param>
    /// <param name="controllerName">ControllerName value</param>
    /// <param name="requestActions">RequestActions value</param>
    internal static void Register(HttpConfiguration httpConfiguration, MethodInfo setSampleRequest, 
                string controllerName, IEnumerable<methodinfo> requestActions, MethodInfo generateObject)
    {
        // --------------------------------------------
        // Full code is available in the source control
        // --------------------------------------------
    }
}

/// <summary> 
/// Register Response Type class
/// </summary>
internal sealed class RegisterResponseTypes
{
    /// <summary>
    /// Register Response types 
    /// </summary>
    /// <param name="httpConfiguration">HttpConfiguration value</param>
    /// <param name="setSampleResponse">SampleReponse MethodInfo value</param>
    /// <param name="controllerName">ControllerName value</param>
    /// <param name="responseActions">ResponseActions value</param>
    internal static void Register(HttpConfiguration httpConfiguration, MethodInfo setSampleResponse, 
                string controllerName, IEnumerable<methodinfo> responseActions, 
                MethodInfo generateObject)
    {
        // --------------------------------------------
        // Full code is available in the source control
        // --------------------------------------------
    }
}

/// <summary>
/// Register API Help class
/// </summary>
public static class RegisterAPIHelp
{
    /// <summary>
    /// Register Response/Request sample API Documentation
    /// </summary>
    /// <param name="httpConfiguration">HttpConfiguration object</param>
    /// <param name="assembly"/>Assembly object</param>
    public static void RegisterRequestResponseHelp
    (this HttpConfiguration httpConfiguration, Assembly assembly = null)
    {
        
        // --------------------------------------------
        // Full code is available in the source control
        // --------------------------------------------
    }
} 

关注点

尽管本文的主要目的是展示如何扩展 Web API 文档,但在此过程中还有很多东西可以从代码中学习,例如

  • 通过反射调用方法
  • 属性用法
  • 序列化
  • 不同的设计原则和模式。例如流畅接口

历史和 GitHub 版本

© . All rights reserved.