ASP.NET MVC Web Api работаем с несколькими xml файлами документации и исправляем ошибки

Готовое решение из nuget ASP.NET MVC Web Api Help Page работает вполне хорошо. Последнее обновление на текущий момент от февраля этого года. Но до сих пор нет возможности использовать несколько проектов в качестве источника документации. 

Такое может понадобится, например, если классы моделей лежат в проекте BLL. В таком случае документация, генерируемая Help Page будет содержать информацию об этих классах, об их свойствах, но не будет содержать xml комментарии. А они очень важны, для полного понимания api. Тем более, когда нет доступа к коду и не понятно где и как используется дальше конкретное свойство. Для решения этой проблемы необходимо выполнить несколько шагов.

1. Во всех проектах, документация которых потребуется в web api help необходимо включить xml файл с документацией.

включение генерации файла комментариев

2. В проекте Web Api настроить Post Build Events.

Post Build Events

Запись вида: copy $(SolutionDir)SubProject\XmlDocument.xml $(ProjectDir)\App_Data\Subproject.xml

3. Далее открываем файл HelpPageConfig. Заменяем

config.SetDocumentationProvider(new XmlDocumentationProvider(
    HttpContext.Current.Server.MapPath(”~/App_Data/XmlDocument.xml”)));

на

config.SetDocumentationProvider(new XmlDocumentationProvider(
    HttpContext.Current.Server.MapPath(”~/App_Data”)));

Таким образом мы будем подключать не 1 файл с документацией, а несколько из указанной директории.

4. Наконец находим файл XmlDocumentationProvider (/Areas/HelpPage/XmlDocumentationProvider). В этом файле

4.1 Заменяем поле _documentNavigator на 

private List<XPathNavigator> _documentNavigators = new List<XPathNavigator>();

4.2 Заменяем конструктор на

public XmlDocumentationProvider(string appDataPath)
{
    if (appDataPath == null)
    {
        throw new ArgumentNullException(“appDataPath”);
    }

var files = new[] { "XmlDocument.xml", "Subproject.xml" };
foreach (var file in files)
{
    XPathDocument xpath = new XPathDocument(Path.Combine(appDataPath, file));
    _documentNavigators.Add(xpath.CreateNavigator());
}

}

4.3 Под конструктором добавляем метод

private XPathNavigator SelectSingleNode(string selectExpression)
{
    foreach (var navigator in _documentNavigators)
    {
        var propertyNode = navigator.SelectSingleNode(selectExpression);
        if (propertyNode != null)
            return propertyNode;
    }
    return null;
}

4.4 На последнем шаге необходимо изменить поиск. Найти _documentNavigator.SelectSingleNode и убрать приставку _documentNavigator (в п. 4.3 мы добавили специальный метод)

После того, как проект скомпилируется в директории App_Data появятся файлы документации. Если проект публикуется средствами Web Deploy (или другими) необходимо, чтобы файлы с xml комментариями были включены в комплект поставки. Для этого добавляем вручную файлы в Solution Explorer и выставляем им свойство Copy to Output Directory: Copy if newer.

copy if newer

Исправление ошибок

С удивлением обнаружил, что если есть 2 типа с одним и тем же названием (но пусть в разных сборках), генератор документации будет бросать исключения (с дублями). Ошибка будет похожа на “web api help Exception message: Self referencing loop detected with type”. Существует 2 варианта решения этой проблемы.

Первое решение

Это решение подходит, если вы имеете прямой доступ к типу, чтобы модифицировать атрибуты. Лично мне не нравится этот способ тем, что если в сборке определены только классы, для того, чтобы использовать атрибут нужно добавить ссылку на WebApiHelpPage сборку (со всеми вытекающими). Способ подойдет если повезло и тип находится в проекте Api. Тем не менее способ должен работать. 

Для этого нужно добавить атрибут ModelName к типу и указать альтернативное название (которое будет использоваться в ссылках). 

Второе решение

Решение заключается в модифицировании (исправлении изначальной ошибки) в поставке Asp.Net Web Api Help Page. Понадобится изменить следующие файлы:

ModelNameHelper

Просто заменяем на:

using System;
using System.Globalization;
using System.Linq;
using System.Reflection;

namespace HelpPageErrorSimulator.Areas.HelpPage.ModelDescriptions
{
internal static class ModelNameHelper
{
// Modify this to provide custom model name mapping.
public static string GetModelName(Type type)
{
ModelNameAttribute modelNameAttribute = type.GetCustomAttribute<ModelNameAttribute>();
if (modelNameAttribute != null && !String.IsNullOrEmpty(modelNameAttribute.Name))
{
return modelNameAttribute.Name;
}

        string modelName = type.FullName;  
        if (type.IsGenericType)  
        {  
            // Format the generic type name to something like: GenericOfAgurment1AndArgument2  
            Type genericType = type.GetGenericTypeDefinition();  
            Type[] genericArguments = type.GetGenericArguments();  
            string genericTypeName = genericType.FullName;  

            // Trim the generic parameter counts from the name  
            genericTypeName = genericTypeName.Substring(0, genericTypeName.IndexOf('`'));  
            string[] argumentTypeNames = genericArguments.Select(t =&gt; GetModelName(t)).ToArray();  
            modelName = String.Format(CultureInfo.InvariantCulture, "{0}Of{1}", genericTypeName, String.Join("And", argumentTypeNames));  
        }  

        return modelName;  
    }  
}  

}

Таким образом в качестве имени будет использоваться FullName типа (включая namespace).

Следующий класс HelpPageSampleGenerator заменяем метод WriteSampleObjectUsingFormatter на:

[SuppressMessage(“Microsoft.Design”, “CA1031:DoNotCatchGeneralExceptionTypes”, Justification = “The exception is recorded as InvalidSample.”)]
public virtual object WriteSampleObjectUsingFormatter(MediaTypeFormatter formatter, object value, Type type, MediaTypeHeaderValue mediaType)
{
if (formatter == null)
{
throw new ArgumentNullException(“formatter”);
}
if (mediaType == null)
{
throw new ArgumentNullException(“mediaType”);
}

object sample = String.Empty;  
MemoryStream ms = null;  
HttpContent content = null;  
try  
{  
    if (formatter.CanWriteType(type))  
    {  
        ms = new MemoryStream();  
        content = new ObjectContent(type, value, formatter, mediaType);  
        formatter.WriteToStreamAsync(type, value, ms, content, null).Wait();  
        ms.Position = 0;  
        StreamReader reader = new StreamReader(ms);  
        string serializedSampleString = reader.ReadToEnd();  
        if (mediaType.MediaType.ToUpperInvariant().Contains("XML"))  
        {  
            serializedSampleString = TryFormatXml(serializedSampleString);  
        }  
        else if (mediaType.MediaType.ToUpperInvariant().Contains("JSON"))  
        {  
            serializedSampleString = TryFormatJson(serializedSampleString);  
        }  

        sample = new TextSample(serializedSampleString);  
    }  
    else  
    {  
        sample = new InvalidSample(String.Format(  
            CultureInfo.CurrentCulture,  
            "Failed to generate the sample for media type '{0}'. Cannot use formatter '{1}' to write type '{2}'.",  
            mediaType,  
            formatter.GetType().FullName,  
            type.FullName));  
    }  
}  
catch (Exception e)  
{  
    sample = new InvalidSample(String.Format(  
        CultureInfo.CurrentCulture,  
        "An exception has occurred while using the formatter '{0}' to generate sample for media type '{1}'. Exception message: {2}",  
        formatter.GetType().FullName,  
        mediaType.MediaType,  
        UnwrapException(e).Message));  
}  
finally  
{  
    if (ms != null)  
    {  
        ms.Dispose();  
    }  
    if (content != null)  
    {  
        content.Dispose();  
    }  
}  

return sample;  

}

Аналогичная ситуация. Заменяем использование Name на FullName.

Self regerencing loop detected

Кроме того, генератор документации сразу же показывает примеры. В случае, если объет содержит ссылку на свой же тип (цикличные ссылки). Добавляем в Application_Start 

GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Serialize;

Комментарии

comments powered by Disqus