Accessing C#|VB|F# XML Code Documentation via Reflection

The code examples in this article are included in the open source Towel project. Please check out the Towel project if you like what you see. 🙂 https://github.com/ZacharyPatten/Towel

Zachary Patten

In this post I will demonstrate how you can easily load C#|VB|F# XML code documentation and access it via reflection (using extension methods).

Note: The method described in this post only works if you are able to get the “.xml” file generated by Visual Studio when the code is built.

Why would you want to do this?

Before I begin explaining how to do it, I first want to explain why you would want to. The main reason is to generate documentation and help files for a website. There are projects out there like SandCastle https://github.com/EWSoftware/SHFB that will generate documentation pages for you, but using those frameworks can be a pain in the butt when you just want to generate raw HTML that you can style yourself and easily embed into an existing website.

What is C#|VB|F# XML code documentation?

XML code documentation is the formatted comments you can place above members of your code. This includes tags like summary, param, returns, exception, and remarks that are supported by Visual Studio. This documentation is extremely useful to guide other people in how to properly use your code.

C# XML Documentation Example
Visual Basic Documentation Example

You are not limited to only the sugguested tags. You can use any tag you want. For example, you can add a tag “runtime” to specify the algorithmic complexity of a function, or you can add a tag “citation” to help credit other programmers for specific members of your code.

What happens to the XML when you build your code?

When you build your project in Visual Studio, the XML documentation is NOT included in the compiled code. There is an option in Visual Studio to write all the XML to a file when you build a project. Because the XML is not embedded in the compiled code, it is not natively accessible via reflection (which is why I’m writing this article).

The setting to export XML documentation is in the project “Properties -> Build/Compile” view

The XML file that is exported by Visual Studio is in a very simple format. The documentation of every member in the project is placed inside it’s own “member” XML element, and the “name” of that element represents the member the documentation belongs to. Here is an example:

Example XML File Format

How can we load and access the XML documentation from code? (Framework Code)

Loading the XML file into memory is very easy. You can do it with the System.Xml.XmlReader class, and we can store all the documentation in a Dictionary<string, string> where the dictionary key is the name of the member as it appears in the XML documentation file. Here is the source code:

internal static Dictionary<string, string> loadedXmlDocumentation = new Dictionary<string, string>();

/// <summary>Loads the XML code documentation into memory so it can be accessed by extension methods on reflection types.</summary>
/// <param name="xmlDocumentation">The content of the XML code documentation.</param>
public static void LoadXmlDocumentation(string xmlDocumentation)
{
    using (XmlReader xmlReader = XmlReader.Create(new StringReader(xmlDocumentation)))
    {
        while (xmlReader.Read())
        {
            if (xmlReader.NodeType == XmlNodeType.Element && xmlReader.Name == "member")
            {
                string raw_name = xmlReader["name"];
                loadedXmlDocumentation[raw_name] = xmlReader.ReadInnerXml();
            }
        }
    }
}

Once the documentation is loaded into memory, all we need to do is write some extension methods that will convert reflection types into the relative dictionary key in the XML documentation format so we can look up the documentation in the dictionary:

/// <summary>Gets the XML documentation on a type.</summary>
/// <param name="type">The type to get the XML documentation of.</param>
/// <returns>The XML documentation on the type.</returns>
/// <remarks>The XML documentation must be loaded into memory for this function to work.</remarks>
public static string GetDocumentation(this Type type)
{
    string key = "T:" + Regex.Replace(type.FullName, @"\[.*\]", string.Empty).Replace('+', '.');

    loadedXmlDocumentation.TryGetValue(key, out string documentation);
    return documentation;
}

/// <summary>Gets the XML documentation on a method.</summary>
/// <param name="methodInfo">The method to get the XML documentation of.</param>
/// <returns>The XML documentation on the method.</returns>
/// <remarks>The XML documentation must be loaded into memory for this function to work.</remarks>
public static string GetDocumentation(this MethodInfo methodInfo)
{
    int genericParameterCounts = methodInfo.GetGenericArguments().Length;
    ParameterInfo[] parameterInfos = methodInfo.GetParameters();

    string key = "M:" +
        Regex.Replace(methodInfo.DeclaringType.FullName, @"\[.*\]", string.Empty).Replace('+', '.') + "." + methodInfo.Name +
        (genericParameterCounts > 0 ? "`" + genericParameterCounts : string.Empty) +
        (parameterInfos.Length > 0 ? "(" + string.Join(",", parameterInfos.Select(x => x.ParameterType.ToString())) + ")" : string.Empty);

    loadedXmlDocumentation.TryGetValue(key, out string documentation);
    return documentation;
}

/// <summary>Gets the XML documentation on a constructor.</summary>
/// <param name="constructorInfo">The constructor to get the XML documentation of.</param>
/// <returns>The XML documentation on the constructor.</returns>
/// <remarks>The XML documentation must be loaded into memory for this function to work.</remarks>
public static string GetDocumentation(this ConstructorInfo constructorInfo)
{
    ParameterInfo[] parameterInfos = constructorInfo.GetParameters();

    string key = "M:" +
        Regex.Replace(constructorInfo.DeclaringType.FullName, @"\[.*\]", string.Empty).Replace('+', '.') + ".#ctor" +
        (parameterInfos.Length > 0 ? "(" + string.Join(",", parameterInfos.Select(x => x.ParameterType.ToString())) + ")" : string.Empty);

    loadedXmlDocumentation.TryGetValue(key, out string documentation);
    return documentation;
}

/// <summary>Gets the XML documentation on a property.</summary>
/// <param name="propertyInfo">The property to get the XML documentation of.</param>
/// <returns>The XML documentation on the property.</returns>
/// <remarks>The XML documentation must be loaded into memory for this function to work.</remarks>
public static string GetDocumentation(this PropertyInfo propertyInfo)
{
    string key = "P:" + Regex.Replace(propertyInfo.DeclaringType.FullName, @"\[.*\]", string.Empty).Replace('+', '.') + "." + propertyInfo.Name;

    loadedXmlDocumentation.TryGetValue(key, out string documentation);
    return documentation;
}

/// <summary>Gets the XML documentation on a field.</summary>
/// <param name="fieldInfo">The field to get the XML documentation of.</param>
/// <returns>The XML documentation on the field.</returns>
/// <remarks>The XML documentation must be loaded into memory for this function to work.</remarks>
public static string GetDocumentation(this FieldInfo fieldInfo)
{
    string key = "F:" + Regex.Replace(fieldInfo.DeclaringType.FullName, @"\[.*\]", string.Empty).Replace('+', '.') + "." + fieldInfo.Name;

    loadedXmlDocumentation.TryGetValue(key, out string documentation);
    return documentation;
}

/// <summary>Gets the XML documentation on an event.</summary>
/// <param name="eventInfo">The event to get the XML documentation of.</param>
/// <returns>The XML documentation on the event.</returns>
/// <remarks>The XML documentation must be loaded into memory for this function to work.</remarks>
public static string GetDocumentation(this EventInfo eventInfo)
{
    string key = "E:" + Regex.Replace(eventInfo.DeclaringType.FullName, @"\[.*\]", string.Empty).Replace('+', '.') + "." + eventInfo.Name;

    loadedXmlDocumentation.TryGetValue(key, out string documentation);
    return documentation;
}

/// <summary>Gets the XML documentation on a member.</summary>
/// <param name="memberInfo">The member to get the XML documentation of.</param>
/// <returns>The XML documentation on the member.</returns>
/// <remarks>The XML documentation must be loaded into memory for this function to work.</remarks>
public static string GetDocumentation(this MemberInfo memberInfo)
{
    if (memberInfo.MemberType.HasFlag(MemberTypes.Field))
    {
        return ((FieldInfo)memberInfo).GetDocumentation();
    }
    else if (memberInfo.MemberType.HasFlag(MemberTypes.Property))
    {
        return ((PropertyInfo)memberInfo).GetDocumentation();
    }
    else if (memberInfo.MemberType.HasFlag(MemberTypes.Event))
    {
        return ((EventInfo)memberInfo).GetDocumentation();
    }
    else if (memberInfo.MemberType.HasFlag(MemberTypes.Constructor))
    {
        return ((ConstructorInfo)memberInfo).GetDocumentation();
    }
    else if (memberInfo.MemberType.HasFlag(MemberTypes.Method))
    {
        return ((MethodInfo)memberInfo).GetDocumentation();
    }
    else if (memberInfo.MemberType.HasFlag(MemberTypes.TypeInfo) ||
        memberInfo.MemberType.HasFlag(MemberTypes.NestedType))
    {
        return ((TypeInfo)memberInfo).GetDocumentation();
    }
    else
    {
        return null;
    }
}

How do you actually use the code?

Now that we have our framework code ready, we can give it a test run. Here is what the usage looks like:

// This function loads in XML documentation so you can access it via reflection.
System.Extensions.LoadXmlDocumentation(File.ReadAllText(@"..\..\..\..\..\Sources\Towel\Towel.xml"));

Console.WriteLine(typeof(Compute).GetDocumentation());

Console.WriteLine(typeof(Constant<float>).GetField(nameof(Constant<float>.Pi)).GetDocumentation());

This code example is taken from the “Extensions” example in the Towel project. You can download the project and test it yourself. It only took a few lines of code and we are already reading XML documentation. 🙂

In order for you to use this code in for your own projects, you just need to swap out the file path to the XML document so that it loads your XML file. Then you can use the extension methods while reflecting on your own types to get their documentation.

How can this be expanded to easily generate an entire webpage or website for documentation?

If you want to expand this so that you can generate documentation for an entire assembly/project, then all you need to do is expand the usage by reflecting through your assembly. Use methods like “Assembly.GetExportedTypes()” to get all publicly visible types from your assembly, and then call the extension method on each type to get it’s documentation.

The following code will loop through all the publicly visible types in the current assembly and write the documentation to the console:

string myXmlFile = /*Insert Your XML File Here*/;
Extensions.LoadXmlDocumentation(File.ReadAllText(myXmlFile));
Assembly assembly = Assembly.GetExecutingAssembly();
foreach (Type type in assembly.GetExportedTypes())
{
    Console.Write(type.GetDocumentation());
}

If you want to see a bit more complicated example, Towel_Documentation project (see the project on GitHub) is reading through the Towel assembly and generating an HTML tree with the documentation on every type, field, method, constructor, etc.

Leave a Reply

Your email address will not be published. Required fields are marked *