XmlCommentDocumentationProvider et fichiers XML

C’est la preview de la prochaine mise à jour d’ASP.NET (ASP.NET Fall 2012) qui m’a convaincu de tester Web API Help Page. Il s’agit en fait de l’intégration d’un package en version alpha sur nuget.

Ce dernier génère la documentation des WebApi à partir de leurs routes et des commentaires de leur contrôleur (commentaires enregistrés dans le fichier XML associé à l’assemblage). Seul inconvénient actuellement: l’implémentation proposée de l’interface IDocumentationProvider (XmlCommentDocumentationProvider) est limitée à un seul fichier XML.

Dans les projets d’envergure, les WebApi seront découpées en plusieurs projets (grâce aux Portable Areas de MvcContrib).

L’idée est donc de pouvoir générer la documentation à partir d’un dossier de fichiers XML, le répertoire /bin par exemple.

Cette implémentation est moins performante que l’originale car elle charge les fichiers nécessaires à la demande (facilement améliorable). Elle ajoute:

  • Le support de plusieurs librairies Web API avec chacune son fichier XML.
  • Prise en compte des commentaires du contrôleur si l’action n’en a pas (ce qui peut être souvent le cas spécifiquement avec les ApiControllers).

L’implémentation originale de XmlCommentDocumentationProvider est de Yao Huang, vous pouvez lire son article détaillé sur son blog msdn.

  public class XmlCommentDocumentationProvider : IDocumentationProvider
    {
        private readonly XPathNavigator _documentNavigator;
        private const string _methodExpression = "/doc/members/member[@name='M:{0}']";
        private const string _typeExpression = "/doc/members/member[@name='T:{0}']";
        private readonly static Regex nullableTypeNameRegex = new Regex(@"(.*\.Nullable)" + Regex.Escape("`1[[") + "([^,]*),.*");

        private readonly Dictionary<string string="string"> _documents;


        public XmlCommentDocumentationProvider(string path)
        {
            if (System.IO.File.Exists(path))
            {
                XPathDocument xpath = new XPathDocument(path);
                _documentNavigator = xpath.CreateNavigator();
            }
            else
            {
                if (!System.IO.Directory.Exists(path))
                    throw new DirectoryNotFoundException(
                                string.Format("Path not found: {0}.", path));
                var files = Directory.GetFiles(
                              path, 
                              "*.xml", 
                              System.IO.SearchOption.TopDirectoryOnly);
                if (files.Length == 0)
                    throw new ConfigurationErrorsException(
                              string.Format("The path provided for the documentation of WebAPI contains no XML file: {0}.", path));
                _documents = files.ToDictionary(
                              key => System.IO.Path.GetFileNameWithoutExtension(key),
                              value => value);
            }
        }

        public virtual string GetDocumentation(HttpParameterDescriptor parameterDescriptor)
        {
            ReflectedHttpParameterDescriptor reflectedParameterDescriptor = parameterDescriptor as ReflectedHttpParameterDescriptor;
            if (reflectedParameterDescriptor != null)
            {
                XPathNavigator memberNode = GetMemberNode(reflectedParameterDescriptor.ActionDescriptor);
                if (memberNode != null)
                {
                    string parameterName = reflectedParameterDescriptor.ParameterInfo.Name;
                    XPathNavigator parameterNode = memberNode.SelectSingleNode(
                              string.Format("param[@name='{0}']", parameterName));
                    if (parameterNode != null)
                    {
                        return parameterNode.Value.Trim();
                    }
                }
            }

            return "No Documentation Found.";
        }

        public virtual string GetDocumentation(HttpActionDescriptor actionDescriptor)
        {
            XPathNavigator memberNode = GetMemberNode(actionDescriptor);
            if (memberNode != null)
            {
                XPathNavigator summaryNode = memberNode.SelectSingleNode("summary");
                if (summaryNode != null)
                {
                    return summaryNode.Value.Trim();
                }
            }

            return "No Documentation Found.";
        }

        private XPathNavigator ResolveNavigator(ReflectedHttpActionDescriptor actionDescriptor)
        {
            string path;
            if (_documents.TryGetValue(
               actionDescriptor.MethodInfo.DeclaringType.Assembly.GetName().Name, 
               out path))
            {
                XPathDocument xpath = new XPathDocument(path);
                return xpath.CreateNavigator();
            }
            return null;
        }

        private XPathNavigator GetMemberNode(HttpActionDescriptor actionDescriptor)
        {
            ReflectedHttpActionDescriptor reflectedActionDescriptor = actionDescriptor as ReflectedHttpActionDescriptor;
            if (reflectedActionDescriptor != null)
            {
                XPathNavigator navigator = _documentNavigator ?? ResolveNavigator(reflectedActionDescriptor);
                if (navigator == null)
                    return null;

                string selectExpression = string.Format(_methodExpression, GetMemberName(reflectedActionDescriptor.MethodInfo));
                XPathNavigator node = navigator.SelectSingleNode(selectExpression) ?? navigator.SelectSingleNode(string.Format(_typeExpression, reflectedActionDescriptor.MethodInfo.DeclaringType.FullName));
                if (node != null)
                {
                    return node;
                }
            }

            return null;
        }

        private static string GetMemberName(MethodInfo method)
        {
            string name = string.Format("{0}.{1}", method.DeclaringType.FullName, method.Name);
            var parameters = method.GetParameters();
            if (parameters.Length != 0)
            {
                string[] parameterTypeNames = parameters.Select(param => ProcessTypeName(param.ParameterType.FullName)).ToArray();
                name += string.Format("({0})", string.Join(",", parameterTypeNames));
            }

            return name;
        }

        private static string ProcessTypeName(string typeName)
        {
            var result = nullableTypeNameRegex.Match(typeName);
            if (result.Success)
            {
                return string.Format("{0}{{{1}}}", result.Groups[1].Value, result.Groups[2].Value);
            }
            return typeName;
        }
    }

Ce fournisseur peut être enregistré depuis la classe HelpPageConfig:

    public static class HelpPageConfig
    {
        public static void Register(HttpConfiguration config)
        {
            config.SetDocumentationProvider(
                new XmlCommentDocumentationProvider(
                    HttpContext.Current.Server.MapPath("~/bin")));
            // [...]
        }
    }