Documentación automática en ASP.NET WebApi y soporte para arquitectura en capas

Buenas!

Como vimos en el post anterior, es muy sencillo el poder tener una documentación automática y detallada de nuestras WebApi’s. Sin embargo, el enfoque propuesto solamente nos sirve cuando todas las clases involucradas están en el mismo proyecto web.

Esto muchas veces no necesariamente es así. En una arquitectura en capas normalmente tendremos un proyecto separado con las clases que representan las entidades del dominio de nuestra aplicación, las cuales formarán parte tanto de requests y responses de la API. Además de que este esquema (con las clases de entidades separadas en otro proyecto) nos permite compartirlas de forma directa (a través de la DLL resultante) a los clientes de la API, por lo cual es una alternativa más que válida para cualquier caso real de implementación.

Veamos el siguiente ejemplo, donde tenemos nuestro proyecto web (WebApi) y una biblioteca de clases con nuestras entidades que interactuan en la API (WebApi.Contracts). En la API tenemos el controlador UsersController que retorna/recibe instancias de User:

DocumentacionWebApiOtrasAssemblies - EstructuraProyecto

Donde la clase User de contratos tiene el siguiente contenido:

namespace WebApi.Contracts
{
using System;
using System.ComponentModel.DataAnnotations;
/// <summary>
/// Representa un usuario de la aplicación.
/// </summary>
public class User
{
/// <summary>
/// Identificador.
/// </summary>
[Required]
public int Id { get; set; }
/// <summary>
/// Nombre.
/// </summary>
[Required]
[MinLength(10)]
[MaxLength(200)]
public string Name { get; set; }
/// <summary>
/// Fecha de nacimiento.
/// </summary>
[Required]
public DateTime BirthDate { get; set; }
}
}

view raw
User.cs
hosted with ❤ by GitHub

Al ejecutar nuestra aplicación e ir a la documentación de la API, veremos que están las propiedades de la clase pero no la documentación que nosotros le hemos agregado:

DocumentacionWebApiOtrasAssemblies - ResultadoOriginal

Entonces, para lograr ambas cosas debemos realizar algunos ajustes en dos partes: en la generación del Xml de la documentación y en la lectura del mismo. El enfoque que vamos a usar es el que se describe en este post de Stack Overflow con algunas variantes.

Generación del Xml de documentación del proyecto de contratos

Esta parte es relativamente sencilla. Lo primero que haremos es configurar que nuestro proyecto WebApi.Contracts genere el archivo Xml con la documentación de nuestras clases, tal cual hemos hecho en el post anterior para el proyecto Web:

DocumentacionWebApiOtrasAssemblies - ConfiguracionProyectoContrato

Lo siguiente que deberemos hacer será dejar disponible ese Xml dentro de la estructura del proyecto Web, en la carpeta App_Data donde se encuentra ya el generado por dicho proyecto

Lo primero que haremos es modificar nuestra configuración en el proyecto WebApi.Contracts, proyecto donde tenemos nuestras entidades compartidas. La forma que recomiendo es a través de un comando de post-compilación del proyecto Web. Para esto iremos a la propiedades del proyecto Web y dentro de la pestaña Build Events pondremos el siguiente comando en la opción Post-build event command line:

copy "$(SolutionDir)WebApi.Contracts\XmlDocument.xml" "$(ProjectDir)\App_Data\XmlDocument_Contracts.xml"

En la primer parte del comando indicamos el documento origen y en la segunda el destino. El único cuidado que deberemos tener es no usar el mismo nombre de archivo para no perder lo del proyecto web, tan solo un detalle.

DocumentacionWebApiOtrasAssemblies - ConfiguracionEventoCompilacion

 

Configuración en la aplicación para leer más de un archivo

Una vez finalizado lo anterior, dentro de la carpeta App_Data del proyecto Web tendremos los dos archivos Xml, uno de cada proyecto. Lo que deberemos hacer es modificar parte de la lógica del área HelpPage para que permita tomar más de un origen de información.

Lo primero será modificar el archivo /Areas/HelpPage/App_Start/HelpPageConfig.cs para que en vez de apuntar al archivo XmlDocument.xml de la carpeta App_Data lo hagamos a la carpeta en si misma:

using System.Diagnostics.CodeAnalysis;
using System.Net.Http.Headers;
using System.Web;
using System.Web.Http;
namespace WebApi.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
{
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")));
}
}
}

view raw
HelpPageConfig.cs
hosted with ❤ by GitHub

Recordemos que esto es solo una porción del archivo, abajo deberá estar todo el contenido original tal cual está.

Ahora nos queda un conjunto de cambios a realizar, todos ellos sobre el archivo /Areas/HelpPage/XmlDocumentationProvider.cs

Los cambios serán los siguientes:

  1. Línea 18: Modificar la variable de instancia _documentNavigator que es del tipo XPathNavigator. Con este cambio que vamos a necesitar debe ser una lista.
  2. Líneas 32-38: Originalmente se creaba el la instancia de XPathNavigator con el path completo que definíamos en el archivo HelpPageConfig.cs. En cambio ahora recibiremos una carpeta, por lo cual lo que haremos es buscar todos los archivos Xml, crear la instancia correspondiente y agregarla a la lista.
  3. Líneas 41-51: Creamos el método SelectSingleNode, el cual nos permite acceder a la información particular de un nodo dentro de todos los archivos que tenemos en la lista. Esto en el archivo original se hacía directamente sobre la instancia de _documentNavigator, pero como ahora es una lista nos dará error de compilación. Lo que haremos ahora es corregir todos esos errores haciendo que se invoque este método que acabamos de crear.
namespace WebApi.Areas.HelpPage
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Web.Http.Controllers;
using System.Web.Http.Description;
using System.Xml.XPath;
using WebApi.Areas.HelpPage.ModelDescriptions;
/// <summary>
/// A custom <see cref="IDocumentationProvider"/> that reads the API documentation from an XML documentation file.
/// </summary>
public class XmlDocumentationProvider : IDocumentationProvider, IModelDocumentationProvider
{
private IList<XPathNavigator> _documentNavigator = new List<XPathNavigator>();
// CÓDIGO ADICIONAL DE LA APLICACIÓN
/// <summary>
/// Initializes a new instance of the <see cref="XmlDocumentationProvider"/> class.
/// </summary>
/// <param name="documentPath">The physical path to XML document.</param>
public XmlDocumentationProvider(string documentPath)
{
if (documentPath == null)
{
throw new ArgumentNullException("documentPath");
}
var files = System.IO.Directory.GetFiles(documentPath, "*.xml");
foreach (var file in files)
{
XPathDocument xpath = new XPathDocument(file);
_documentNavigator.Add(xpath.CreateNavigator());
}
}
private XPathNavigator SelectSingleNode(string selectExpression)
{
foreach (var navigator in _documentNavigator)
{
var propertyNode = navigator.SelectSingleNode(selectExpression);
if (propertyNode != null)
return propertyNode;
}
return null;
}
// CÓDIGO ADICIONAL DE LA APLICACIÓN
}
}

Una vez finalizados estos pasos podemos probar nuevamente la ejecución de la aplicación y ver que el resultado es el esperado:

DocumentacionWebApiOtrasAssemblies - ResultadoFinal

 

De esta forma, con algunos ajustes sobre el código inicial, podemos mantener el enfoque de que nuestras WebApi’s estén documentadas de forma automática tomando como origen la documentación del código, incluida la de otros proyectos adicionales.

Espero que les sirva, gracias por leer!!

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s