«
»


Obtaining inline XML C# documentation at runtime using .NET 2.0

Posted by jimblackler on Mar 23, 2008

C# .NET provides a standard mechanism for programmers to inline XML documentation in their programs. Classes, functions, properties and more can be augmented with comments, and the build system creates amalgamated XML files accompanying the program output. A variety of tools such as Sandcastle can build help files from the data. Visual Studio 2005 and 2008 also supports the scheme with keyboard shortcuts and optional build warnings for missing comments.

  1. /// <summary>
  2. /// An example C# class
  3. /// </summary>
  4. /// <remarks>
  5. /// This class illustrates how a class can be marked up with inline C# comments
  6. /// </remarks>
  7. class SomeExampleClass
  8. {
  9.     /// <summary>
  10.     /// An example of a property
  11.     /// </summary>
  12.     public int ExampleProperty
  13.     {
  14.         get { return somePrivateVar; }
  15.     }
  16. }

Unfortunately, many programmers have observed that there’s no way to discover these comments at runtime by reflection. All manner of alternative information can be accessed with reflection – but not the XML comments.

The solution

This documentation can be discovered at run time with a little extra code, as I will demonstrate. The reason the comments can’t be discovered by reflection alone is because they are not included in the .NET assemblies (.EXE or .DLL files), but are conventionally included as .XML files to accompany the assembly files.

I’ve provided a simply class library for .NET 2.0 called DocsByReflection that will when possible return an XmlElement that describes a given type or member. This can be used to easily extract comments at runtime. Here’s an example of how:

  1. XmlElement documentation = DocsByReflection.XMLFromMember(typeof(SomeExampleClass).GetProperty("ExampleProperty"));
  2. Console.WriteLine(documentation["summary"].InnerText.Trim());

This would ouput “An example of a property”, extracted from the code comments in the code fragment at the top of the article.

The class works by locating the .XML file accompanying the assembly that defines the type or member (discovered using reflection). Then the .XML file is loaded into an XmlDocument, and the ‘member’ tags are scanned to find the one that refers to the target type or member. This tag is returned as an XmlElement which contains the free form XML that these comments can contain (e.g. including HTML markup).

This method relies on the existence, on disk, of the generated XML. It must have the same name and location as the generated .EXE or .DLL accompanying it – with only the extension changed. This will happen by default when building with Visual Studio, once documentation is enabled (see below). If you are distributing your program and expect the distributed version to discover the comments in run time, you must also distribute the XMLs alongside the executables. This is commonly seen in any case. Note that many of the framework executables (for instance those found in C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727) are accompanied by documentation XML files.

How to use

Download the library source as a zip file here, or use Subversion to update to the very latest version here and here.

(Updated 18/3/08 for bug fix – see comments).

Load the DocsByReflectionDemo.sln to see a simple example of how to use the library. This gathers and prints information about a class type, some functions, a property and a field.

Using in your own code

In Visual Studio, set up your own project that includes XML comments.

(Note that the XML documentation output is not enabled by default in Visual Studio. Go to your project properties, select Build, and under the Output section check the box that says XML Documentation File. It is very important that you do not change the location of the XML file or this method will not be able to locate it at run time.)

If you have not already, add the comments in Visual Studio. By default, pressing forward slash three times when the caret is positioned before the start of an element you wish to document (such as a class or method) will insert a documentation template for you before the start of the file.

Add the DocsByReflection project to your solution and a Reference to it from your project. Add the line “using JimBlackler.DocsByReflection;” to the top of your .CS file. The call se the functions DocsByReflection.XMLFromType() (for classes, structures and other types) and DocsByReflection.XMLFromMember() (for methods, properties and fields) to fetch XmlElement objects that represent the documentation for that type or member.

Query the returned object to read the documentation. For instance, xmlElement[“summary”].InnerText.Trim(). This locates the summary node, and uses the InnerText property of XmlElement to strip out any XML formatting that may be embedded in the comment. Also, Trim() or a regular expression can strip the unwanted whitespace from the comment.

Comments or problems

If you have any comments or problems with the library, please add a reply to this blog post and I will answer them here.

21 Comments »

Courtney:

This is great, but I’m trying to used it to get the summary information from core .NET assemblies such as System.Web. The related assemblies are “not found.”

Yet, I believe they are somewhere because you can get the summary information from intellisense or from “go to definition” command and then looking for it in the class view (from metadata).

Any ideas of how to get documentation info from core libraries?

April 8th, 2008 | 5:55 am
jimblackler:

Hi Courtney, thanks for your comment.

I did spend a few hours trying to find a solution for this actually. The problem is that system assemblies can end up in the Global Assembly Cache (GAC) directory (which is the only known location for them at runtime), seperated from their .XML files. These files do exist, but in the original directory.

The solution will be to find some way of locating the original .DLL file given the GACed .dll file details.

Since there is demand I may put aside some time this week to have a try at discoving a way to do this.

April 8th, 2008 | 9:19 am
Courtney:

Hi Jim,

I just modded your code to add the ability to specify a basepath:

private static XmlDocument XMLFromAssemblyNonCached(Assembly assembly, String basePath)
{
string assemblyFilename = assembly.CodeBase;
Regex thisResx = new Regex(@”(.*)[\/\\]([^\/\\]+\.\w+)$”, RegexOptions.IgnoreCase | RegexOptions.Multiline);
Match thisMatch = thisResx.Match(assemblyFilename);

const string prefix = “file:///”;
assemblyFilename = prefix + basePath + thisMatch.Groups[2];

if (assemblyFilename.StartsWith(prefix))
{
StreamReader streamReader;

try
{
streamReader = new StreamReader(Path.ChangeExtension(assemblyFilename.Substring(prefix.Length), “.xml”));
}
catch (FileNotFoundException exception)
{
throw new DocsByReflectionException(“XML documentation not present (make sure it is turned on in project properties when building)”, exception);
}

XmlDocument xmlDocument = new XmlDocument();
xmlDocument.Load(streamReader);
return xmlDocument;
}
else
{
throw new DocsByReflectionException(“Could not ascertain assembly filename”, null);
}
}

April 10th, 2008 | 12:05 am
jimblackler:

Thanks Courtney I’ll look at updating the code to reflect that.

Ideally I would like to have it discover the non-GAC location automatically.

April 11th, 2008 | 9:07 am
Alex:

Hi Jim, this is a great work!

I got a small problem (exception), if I try to get docs for methods without args. As shown here, an XML-file-definition has no “()” brackets as long as the method has an empty ergument list:

This method should write to a file. We write to the console to see the effect this object keeps no state.

After a small improvement in XMLFromMember all works perfectly.

Here is my bug-fix:
//AL: 15.04.2008 ==> BUG-FIX remove “()” if parametersString is empty
if (parametersString.Length > 0)
return XMLFromName(methodInfo.DeclaringType, ‘M’, methodInfo.Name + “(” + parametersString + “)”);
else
return XMLFromName(methodInfo.DeclaringType, ‘M’, methodInfo.Name);

April 15th, 2008 | 4:16 pm
jimblackler:

Great spot Alex I will update the code when I next get to a Windows PC.

April 15th, 2008 | 5:40 pm
jimblackler:

Update for Alex’s fix. Courtney I am still intending to find a way to avoid the user having to manually locate the .XML for GACed assemblies as I think it would be neater, if I cannot I will implement your fix.

April 18th, 2008 | 9:33 pm
Gummy:

I think you will find this quite interesting :)

http://web.archive.org/web/20070825074010/http://msdn.microsoft.com/msdnmag/issues/04/06/NETMatters/

Tip :
System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory()

gives “C:\Windows\Microsoft.NET\Framework\v2.0.50727”

Note : my xml files are located in the “en” subdirectory…

Best regards,
Jeremy

June 9th, 2008 | 10:49 pm
jimblackler:

Wow… that’s basically my article, but four years before.

I’ll implement the fix for GACed assemblies.

Thanks Jeremy.

June 10th, 2008 | 12:30 am
Anand:

HI Jim,

I’m getting an error when I say this:

MethodInfo info = Type.GetType ().GetMethod();
XmlElement documentation = DocsByReflection.XMLFromMember(info);

The error is {“Unable to cast object of type ‘System.Xml.XmlComment’ to type ‘System.Xml.XmlElement’.”}

any idea what could be the reason?

NOTE: The same above statements used to work perfectly fine till a few months back, but no more. I don’t know what did I change in the project.

February 26th, 2010 | 9:50 am
Yonas:

Hi Jim,

Does anyone get fix for the ”Unable to cast object of type ‘System.Xml.XmlComment’ to type ‘System.Xml.XmlElement’.” error ?

May 6th, 2010 | 9:47 pm
Oliver:

Hi Jim,

great stuff, works fine. Your code saved a lot of typing work in my project.

Thanks!

/Olli

August 3rd, 2010 | 10:00 am
Rasmus Schultz:

Arnand and Yonas,

I ran into the same problem with VS2010.

Looks like a node is not always an element – I guess newer versions of VS insert a comment into the generated XML, and Jim’s code was not built to handle those.

I posted a fix here:

https://gist.github.com/815092

February 7th, 2011 | 9:13 pm
Rasmus Schultz:

On another note, I needed access to the class in several different parts of my application, so I wrapped that functionality in an extension method for System.Type:

https://gist.github.com/816498

Note that my implementation returns the name of the class when no is available – you may want to change that to return NULL or an empty string. In my case, I needed something to display under any circumstances, so…

Also, you should be able to extend this concept to include other members and other documentation tags…

February 8th, 2011 | 3:20 pm
Rasmus Schultz:

(aw, snap, my tags just got filtered… mr. Blackler, please correct the missing <summary> tags in my last post? thx)

February 8th, 2011 | 3:22 pm

Unfortunately this doesn’t work for .NET Framework classes :(

April 8th, 2011 | 4:22 pm

Your code does not return summary for Generic Type, Methods a.s.o. :(
Have you ever solved the issue to generate the correct String ID that the compiler makes for each code construct. ???

April 15th, 2011 | 8:31 am
Dave Ballantyne:

Hi Jim, Paw; et al:

Jim – After several years there still seems to be demand for this outstanding code. I had the need to port to VB for an app I’m working on.

For anyone interested, I have modded the code and made several improvements including:

* Fixed GAC assemblies issue so doc is now found for CLR 2.0, 3.0, 3.5 and 4.0 System Framework assemblies. (Note: extension assemblies in the GAC are still not supported due to the fact that you will never know where (if at all) the 3rd party xml file resides.)

* Fixed issue for overloaded Constructors and Methods by generating the correct parameter signature, thus creating the correct unique name.

* Above fixes have also fixed finding Generic Types as referenced in Paw’s message above.

* Added optional parameter to return InnerXML vs. InnerText, so in-line XML formatting can be returned if desired.

Jim; If you would like me to re-port back to C# and send to you please drop me a line.

Paw et al;

If you would like either the VB or C# port then please contact me at davidba [at] modao [dot] com [dot] au.

Please note that the VB port has been renamed from DocsByReflection to ReflectedDocComments to maintain naming conventions with my current app.

Regards

Dave

April 22nd, 2011 | 4:46 pm
Tobias:

Hi there Jim and thank you for excellent work with the XML comments. I used your code successfully, but added support for enumerations. Please get back to me if you are interested in the updated source code.

Best regards Tobias Leverin

June 11th, 2012 | 11:15 am
Jens Madsen:

The ‘System.Runtime.InteropServices.RuntimeEnvironment’ solution is OK locating runtime assemblies, but try out this one:

“Microsoft.Build.Utilities.ToolLocationHelper.GetPathToDotNetFrameworkReferenceAssemblies(Microsoft.Build.Utilities.TargetDotNetFrameworkVersion.VersionLatest)”

The above directory (and its subdirectories) contains BOTH the .DLL assembly AND its ‘companion’ .XML

(from: http://msdn.microsoft.com/en-us/library/microsoft.build.utilities.toollocationhelper)

July 18th, 2012 | 3:16 pm
Alex Hayton:

It turns out this is still the best way to get this reflection working. I found another corner case – When using generics in Visual Studio 2008 (.NET 3.5) the check against the generated XML includes the type argument in square brackets.

This isn’t quite right so we need to strip them out. Here’s the version of the function that I used:


private static XmlElement XMLFromName(Type type, char prefix, string name)
{
string fullName;
string fullTypeName = Regex.Replace(type.FullName, @"\[\[.*?\]\]", "");
if (String.IsNullOrEmpty(name))
{
fullName = prefix + ":" + fullTypeName;
}
else
{
fullName = prefix + ":" + fullTypeName + "." + name;
}

XmlDocument xmlDocument = XMLFromAssembly(type.Assembly);

XmlElement matchedElement = null;

foreach (XmlNode xmlNode in xmlDocument["doc"]["members"])
{
if (!(xmlNode is XmlElement))
continue;

var xmlElement = (XmlElement)xmlNode;

if (!xmlElement.Attributes["name"].Value.Equals(fullName))
continue;

if (matchedElement != null)
{
throw new DocsByReflectionException("Multiple matches to query", null);
}

matchedElement = xmlElement;
}

if (matchedElement == null)
{
throw new DocsByReflectionException("Could not find documentation for specified element", null);
}

return matchedElement;
}

January 31st, 2014 | 12:51 pm
Leave a Reply

Comment