XmlDocumentationExtensions

// Reading XML Documentation at Run-Time
// Bradley Smith - 2010/11/25

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using System.Xml.XPath;

/// <summary>
/// Provides extension methods for reading XML comments from reflected members.
/// </summary>
public static class XmlDocumentationExtensions {

    private static Dictionary<string, XDocument> cachedXml;

    /// <summary>
    /// Static constructor.
    /// </summary>
    static XmlDocumentationExtensions() {
        cachedXml = new Dictionary<string, XDocument>(StringComparer.OrdinalIgnoreCase);
    }

    /// <summary>
    /// Returns the expected name for a member element in the XML documentation file.
    /// </summary>
    /// <param name="member">The reflected member.</param>
    /// <returns>The name of the member element.</returns>
    private static string GetMemberElementName(MemberInfo member) {
        char prefixCode;
        string memberName = (member is Type)
            ? ((Type)member).FullName                               // member is a Type
            : (member.DeclaringType.FullName + "." + member.Name);  // member belongs to a Type

        switch (member.MemberType) {
            case MemberTypes.Constructor:
                // XML documentation uses slightly different constructor names
                memberName = memberName.Replace(".ctor", "#ctor");
                goto case MemberTypes.Method;
            case MemberTypes.Method:
                prefixCode = 'M';

                // parameters are listed according to their type, not their name
                string paramTypesList = String.Join(
                    ",",
                    ((MethodBase)member).GetParameters()
                        .Cast<ParameterInfo>()
                        .Select(x => x.ParameterType.FullName
                    ).ToArray()
                );
                if (!String.IsNullOrEmpty(paramTypesList)) memberName += "(" + paramTypesList + ")";
                break;

            case MemberTypes.Event:
                prefixCode = 'E';
                break;

            case MemberTypes.Field:
                prefixCode = 'F';
                break;

            case MemberTypes.NestedType:
                // XML documentation uses slightly different nested type names
                memberName = memberName.Replace('+', '.');
                goto case MemberTypes.TypeInfo;
            case MemberTypes.TypeInfo:
                prefixCode = 'T';
                break;

            case MemberTypes.Property:
                prefixCode = 'P';
                break;

            default:
                throw new ArgumentException("Unknown member type", "member");
        }

        // elements are of the form "M:Namespace.Class.Method"
        return String.Format("{0}:{1}", prefixCode, memberName);
    }

    /// <summary>
    /// Returns the XML documentation (summary tag) for the specified member.
    /// </summary>
    /// <param name="member">The reflected member.</param>
    /// <returns>The contents of the summary tag for the member.</returns>
    public static string GetXmlDocumentation(this MemberInfo member) {
        AssemblyName assemblyName = member.Module.Assembly.GetName();
        return GetXmlDocumentation(member, assemblyName.Name + ".xml");
    }
    
    /// <summary>
    /// Returns the XML documentation (summary tag) for the specified member.
    /// </summary>
    /// <param name="member">The reflected member.</param>
    /// <param name="pathToXmlFile">Path to the XML documentation file.</param>
    /// <returns>The contents of the summary tag for the member.</returns>
    public static string GetXmlDocumentation(this MemberInfo member, string pathToXmlFile) {
        AssemblyName assemblyName = member.Module.Assembly.GetName();
        XDocument xml = null;

        if (cachedXml.ContainsKey(assemblyName.FullName))
            xml = cachedXml[assemblyName.FullName];
        else
            cachedXml[assemblyName.FullName] = (xml = XDocument.Load(pathToXmlFile));

        return GetXmlDocumentation(member, xml);
    }

    /// <summary>
    /// Returns the XML documentation (summary tag) for the specified member.
    /// </summary>
    /// <param name="member">The reflected member.</param>
    /// <param name="xml">XML documentation.</param>
    /// <returns>The contents of the summary tag for the member.</returns>
    public static string GetXmlDocumentation(this MemberInfo member, XDocument xml) {
        return xml.XPathEvaluate(
            String.Format(
                "string(/doc/members/member[@name='{0}']/summary)",
                GetMemberElementName(member)
            )
        ).ToString().Trim();
    }

    /// <summary>
    /// Returns the XML documentation (returns/param tag) for the specified parameter.
    /// </summary>
    /// <param name="parameter">The reflected parameter (or return value).</param>
    /// <returns>The contents of the returns/param tag for the parameter.</returns>
    public static string GetXmlDocumentation(this ParameterInfo parameter) {
        AssemblyName assemblyName = parameter.Member.Module.Assembly.GetName();
        return GetXmlDocumentation(parameter, assemblyName.Name + ".xml");
    }

    /// <summary>
    /// Returns the XML documentation (returns/param tag) for the specified parameter.
    /// </summary>
    /// <param name="parameter">The reflected parameter (or return value).</param>
    /// <param name="pathToXmlFile">Path to the XML documentation file.</param>
    /// <returns>The contents of the returns/param tag for the parameter.</returns>
    public static string GetXmlDocumentation(this ParameterInfo parameter, string pathToXmlFile) {
        AssemblyName assemblyName = parameter.Member.Module.Assembly.GetName();
        XDocument xml = null;

        if (cachedXml.ContainsKey(assemblyName.FullName))
            xml = cachedXml[assemblyName.FullName];
        else
            cachedXml[assemblyName.FullName] = (xml = XDocument.Load(pathToXmlFile));

        return GetXmlDocumentation(parameter, xml);
    }

    /// <summary>
    /// Returns the XML documentation (returns/param tag) for the specified parameter.
    /// </summary>
    /// <param name="parameter">The reflected parameter (or return value).</param>
    /// <param name="xml">XML documentation.</param>
    /// <returns>The contents of the returns/param tag for the parameter.</returns>
    public static string GetXmlDocumentation(this ParameterInfo parameter, XDocument xml) {
        if (parameter.IsRetval || String.IsNullOrEmpty(parameter.Name))
            return xml.XPathEvaluate(
                String.Format(
                    "string(/doc/members/member[@name='{0}']/returns)",
                    GetMemberElementName(parameter.Member)
                )
            ).ToString().Trim();
        else
            return xml.XPathEvaluate(
                String.Format(
                    "string(/doc/members/member[@name='{0}']/param[@name='{1}'])",
                    GetMemberElementName(parameter.Member),
                    parameter.Name
                )
            ).ToString().Trim();
    }

}