This question was asked on Stack Overflow recently, and it got me thinking about a more elegant and efficient method for reading the XML documentation (comments) from a class, method, property, etc at run-time. As i’m currently reading a book on LINQ, I figured this would be a good chance to make use of the new XDocument class from LINQ-to-XML.

Before I continue, one begs the question as to why you’d need to read XML comments at run-time. Well, I can think of these reasons, at the very least:

  • Avoiding duplication when adding design-time support (since your XML doc usually echoes what you put into DescriptionAttribute)
  • Generating API documentation using a combination of reflection and XML comments
  • Providing more meaningful error messages when a method/class is invoked improperly by the caller

Navigating XML documentation

We all use XML comments and know how they’re structured in code, but the way that they translate into a complete XML document requires some explanation. When you enable the generation of XML documentation for a project, it will spit out an XML file with the same name as the assembly/executable, e.g. SomeProject.dll produces SomeProject.xml. The document is structured as follows:

<?xml version="1.0" ?>
<doc>
  <assembly>
    <name>SomeProject</name>
  </assembly>
  <members>
    <member name="T:SomeProject.SomeClass">
      <summary>Documentation for SomeClass.</summary>
    </member>
    <member name="M:SomeProject.SomeClass.SomeMethod(System.String)">
      <summary>Documentation for SomeMethod.</summary>
      <param name="someParam">Documentation for someParam.</param>
      <returns>Documentation for return value.</returns>
    </member>
    <!-- ... -->
  </members>
</doc>

Of note in the above example are the following:

  • There is no hierarchical structure beyond <members> – all classes, methods, properties and even nested types appear in one massive flat list.
  • There is a very specific naming convention for the name attribute on the <member>tag:
    • Starts with a prefix character: T=type, M=method/constructor, P=property, E=event, F=field
    • The prefix is followed by a colon (:)
    • This is followed by the full name of the member, including the namespace
    • Method/constructor parameters are identified by their type (not their name) and are separated by commas (without spaces)

Furthermore:

  • Constructors are named as #ctor instead of the reflected name .ctor
  • Nested types are named as OwningType.NestedType instead of the reflected name OwningType+NestedType

Translating from a reflected member

Bearing in mind the above, the process for obtaining the value of a name attribute for a member is fairly simple. Given a reflected member of type MemberInfo (the base class from which Type, MethodInfo, PropertyInfo, etc descend):

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:
        memberName = memberName.Replace(".ctor", "#ctor");
        goto case MemberTypes.Method;
    case MemberTypes.Method:
        prefixCode = 'M';
        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:
        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");
}

return String.Format("{0}:{1}", prefixCode, memberName);

Note the use of LINQ to effortlessly transform the array of ParameterInfo objects into a comma-separated list of strings. Now that we have the name that we expect to be able to locate in the XML documentation, we can read the comments.

Reading comments using XDocument and XPath

LINQ-to-XML introduces XDocument, designed to overcome the enormous list of shortcomings and complexities relating to XmlDocument. It is now the preferred object model for reading, querying and otherwise operating upon XML documents (or even fragments, the implementation doesn’t care).

Assuming the XML documentation file is in the same location as the executable, it’s ludicrously simple to get the XML comments for a reflected member:

AssemblyName assemblyName = member.Module.Assembly.GetName();
XDocument xml = XDocument.Load(assemblyName.Name + ".xml");
return xml.XPathEvaluate(
    String.Format(
        "string(/doc/members/member[@name='{0}']/summary)",
        GetMemberElementName(member)
    )
).ToString().Trim();

As you can see above, it’s just a simple XPath expression which returns the text within the <summary> node for the appropriate member.

The process for getting the documentation for a <param> or <returns> node is only slightly more complicated. Given a ParameterInfo instance:

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();

Putting it altogether with extension methods

Extension methods are brilliantly suited to the task of providing an intuitive entry point for this functionality. Since reflected members all descend from MemberInfo (except parameters, which are of the type ParameterInfo as previously indicated), we can define a GetXmlDocumentation() extension method for MemberInfo:

public static string GetXmlDocumentation(this MemberInfo member) { /* ... */ }

…and a separate one for ParameterInfo:

public static string GetXmlDocumentation(this ParameterInfo parameter) { /* ... */ }

This means that calling the method is as simple as:

Console.WriteLine(typeof(SomeClass).GetMethod("SomeMethod").GetXmlDocumentation());
Console.WriteLine(typeof(SomeClass).GetMethod("SomeMethod").GetParameter("someParam").GetXmlDocumentation());
Console.WriteLine(typeof(SomeClass).GetMethod("SomeMethod").ReturnParameter.GetXmlDocumentation());

So, there you have it. My full implementation offers some overloads, as well as a mechanism for caching the XML data for each queried assembly, however these are fairly trivial additions. The real guts of the implementation have been described above.

I hope this code helps you leverage your XML comments :)

Download

XmlDocumentationExtensions.cs

Share/Bookmark

9 thoughts on “Reading XML Documentation at Run-Time

  1. vote

    Hi,

    I was the guy who originally asked the question. Just want to add a reason why this is needed.

    What I’m doing is showing this info for the end user as a Hint.
    This is quite useful when a business object has a lot of properties,
    and You’re not sure which one should be used in which situation.

    Helps bot the user and the programmer.

    Reply
  2. vote

    PropertyInfo extends MemberInfo, so it’s covered by the extension method. To get information about a property, you would use the following syntax:

    Console.WriteLine(typeof(SomeClass).GetProperty(“SomeProperty”).GetXmlDocumentation());

    Reply
  3. vote

    If you replace the ‘+’ with a ‘.’ subclasses will work to.

    public static string GetXmlDocumentation(this MemberInfo member, XDocument xml)
    {
    String expression = String.Format( “string(/doc/members/member[@name='{0}']/summary)”, GetMemberElementName( member ) );
    expression = expression.Replace( “+”, “.” );

    return xml.XPathEvaluate( expression ).ToString().Trim();
    }

    Reply
  4. vote

    There’s some more issues with parameter names, i.e. nested types and generic parameters. Try this as a test case:

    public class Program
    {
    public class Nested
    {
    /// blubb
    public static T MyTest1(T? x, IEnumerable n) where T : struct
    {
    return default(T);
    }
    }

    Cheers,
    Jens.

    Reply
    • vote

      Again… with HTML characters escaped. Hope this works better:

      public class Program
      {
      public class Nested
      {
      /// <summary>blubb</summary>
      public static T MyTest1<T>(T? x, IEnumerable<Nested> n) where T : struct
      {
      return default(T);
      }
      }
      }

      Reply

Leave a reply

required

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>