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 🙂
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.
BTW very good and comprehensive post. Thanks 🙂
what about Property Info?
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());
It is a very good and simple library. Thanks
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();
}
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.
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);
}
}
}
Just wanted to thank you for these extension methods.
I am using it to generate the API docs for my web app. I look for all the Actions in my MVC controller classes that have the RouteAttribute, and I generate an html page based on their xml documentation.
This is the result: [linked removed by moderator]
yeah, nice. But will fail when generics come into the game 😉
Really great thanks! However it doesn’t work with comments.
Correct. The compiler will only include XML comments (triple slash) in the resulting documentation file.
Loading the xml file won’t work, when you are using this in a WebAPI project (as it gets the folder of the Webserver executable). Seems that the CodeBase Property is safer to use:
AssemblyName assemblyName = Assembly.GetExecutingAssembly().GetName();
string codeBase = assemblyName.CodeBase;
string codeBaseDir = Path.GetDirectoryName(codeBase);
string dir = codeBaseDir.Substring(6);
StreamReader streamReader = new StreamReader(dir + @”\” + assemblyName.Name + “.xml”, Encoding.UTF8, true);
XDocument xDoc = XDocument.Load(streamReader);
Thanks for the code. Still works after 10 Years. 🙂