Doxie - Help Generator for .NET Assemblies
Introduction
So you've worked hard on that open source project (or even a commercial one) and it's time to release it, but you realize it's also now that dreaded time of documenting all your code. Going through every public type, constructor, method and property to write your XML comments is hard work already, but then you need to get that into HTML for your website and style it too. You could use XSL or you could even fork out hundreds of dollars for an expensive commercial solution, which is okay if you need a lot of features. But if you're on a tight budget and especially if you're working on an open source project, you may be okay with something quick and simple. Doxie may not be the most elegant solution, but she sure gets the job done. ;-)
Demo
You can find a working demo I deployed to GitHub Pages for another project of mine called, "Extenso": https://gordon-matt.github.io/Extenso/.
Getting Started
- Install NodeJS
- Install JSPM globally:
npm install -g jspm
- Clone/download the project
- Restore JSPM packages:
jspm install
Note: Do this from the root directory of the "Doxie" project (not the solution root).
Further Note: I have selected the JSPM build system as the default because of its ease of use. You can swap out JSPM for webpack if you prefer. See Aurelia's documentation for how to do that..
Generating the Help File
The Aurelia web app is simply an SPA that reads a JSON file for its data. We need to use one of the provided "Help File Generators" to read your assemblies and generate this JSON file. Two generators are provided - one is a WinForms app that lets you select the folder with assemblies in it and which of those to generate help for. The other is a console app and does the same thing, but is provided in case you're running from Linux or Mac - just modify the paths you want in Program.cs and run it.
Creating a Help File with the WinForms Generator
To create a file with the Winforms version of the Help File Generator, follow these steps:
- Start the app
- Click the "Browse" button and select a folder with your .NET assemblies
- Check the boxes of the assemblies that you want to generate documentation for
- Click OK
- After a few short moments, you should receive a message telling you it succeeded.
Once you've generated the file, all you need to do is place it in the js folder in Doxie.
Customization
Before deploying, modify the site as you like. Some suggestions:
You can even add more pages to the Aurelia site and so the generated help documentation will just be a part of it. Just move the help stuff to a new page instead of the home page and create a new home page, about page, contact page, whatever you like. Then all you need to do is drop in a new JSON help file whenever you want to update the documentation.
.NET Core Assemblies
For .NET Core assemblies, you need to ensure that all related assemblies are present in the same location as the ones you want to generate pages from. Otherwise, the resulting documentation will contain error messages caused by FileNotFoundException
s. It can be a pain to figure out what assemblies you need to copy, so there's a simple trick you can use to make this very easy:
- Add a new project to your solution and call it something like
DoxieDummy
or whatever you prefer. - Reference all the projects in the solution that you want documentation for.
- Now the important part: Edit the .csproj file for this dummy project and add the following:
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
. This will ensure ALL assemblies are copied locally to the output directory. Below is a screenshot example:
- Now all you have to do is pass that directory path to one of the "Help File Generators" and tell it which of those assemblies should be documented.
How the Documentation Generation Works
There are 2 parts to how the documentation is generated:
- Parsing the XML files and merging that with data it obtains from reflection of the assembly and its types
- Serializing the in-memory data to a JSON file
Parsing the XML Files
There are multiple ways to parse the XML. One option would be to build some classes to represent the various elements, decorate the properties with the require attributes and then use XML serialization to deserialize the file contents to an object. My original solution was as follows:
[XmlRoot("doc")]
public class DocCommentsFile
{
public DocCommentsFile()
{
Members = new List<MemberElement>();
}
[XmlElement("assembly")]
public AssemblyElement Assembly { get; set; }
[XmlArray("members")]
[XmlArrayItem("member")]
public List<MemberElement> Members { get; set; }
public static DocCommentsFile Open(string path)
{
string text = File.ReadAllText(path);
return text.XmlDeserialize<DocCommentsFile>();
}
}
public class AssemblyElement
{
[XmlElement("name")]
public string Name { get; set; }
}
public class MemberElement
{
public MemberElement()
{
Params = new List<ParamElement>();
TypeParams = new List<ParamElement>();
Exceptions = new List<ExceptionElement>();
}
[XmlAttribute("name")]
public string Name { get; set; }
[XmlElement("summary")]
public SummaryElement Summary { get; set; }
[XmlElement("example")]
public string Example { get; set; }
[XmlElement("param")]
public List<ParamElement> Params { get; set; }
[XmlElement("returns")]
public ReturnsElement Returns { get; set; }
[XmlElement("remarks")]
public string Remarks { get; set; }
[XmlElement("typeparam")]
public List<ParamElement> TypeParams { get; set; }
[XmlElement("exception")]
public List<ExceptionElement> Exceptions { get; set; }
}
public class SummaryElement
{
[XmlElement("see")]
public SeeElement See { get; set; }
}
public class ParamElement : IXmlSerializable
{
[XmlAttribute("name")]
public string Name { get; set; }
[XmlText]
public string Description { get; set; }
#region IXmlSerializable Members
public XmlSchema GetSchema()
{
return null;
}
public void ReadXml(XmlReader reader)
{
reader.MoveToContent();
Description = reader.ReadInnerXml();
}
public void WriteXml(XmlWriter writer)
{
}
#endregion IXmlSerializable Members
}
public class SeeElement
{
[XmlAttribute("cref")]
public string CRef { get; set; }
}
public class TypeParamElement
{
[XmlAttribute("name")]
public string Name { get; set; }
[XmlText]
public string Description { get; set; }
}
public class ExceptionElement : IXmlSerializable
{
[XmlAttribute("cref")]
public string CRef { get; set; }
[XmlText]
public string Description { get; set; }
#region IXmlSerializable Members
public XmlSchema GetSchema()
{
return null;
}
public void ReadXml(XmlReader reader)
{
reader.MoveToContent();
Description = reader.ReadInnerXml();
}
public void WriteXml(XmlWriter writer)
{
}
#endregion IXmlSerializable Members
}
public class ReturnsElement : IXmlSerializable
{
[XmlText]
public string Description { get; set; }
#region IXmlSerializable Members
public XmlSchema GetSchema()
{
return null;
}
public void ReadXml(XmlReader reader)
{
reader.MoveToContent();
Description = reader.ReadInnerXml();
}
public void WriteXml(XmlWriter writer)
{
}
#endregion IXmlSerializable Members
}
This works to some degree, but I did end up with various issues due to elements within the text of other elements. For example, the <see>
elements. I tried to deal with this by implementing IXmlSerializable
on some of the elements, as you can see in the code above. However, it felt a bit "hacky" and I was concerned about similar scenarios. So before attempting to deal with all of that, I decided to look for an existing solution.
I came across a project called NuDoq and I attempted using that, but it wasn't really for me - all I wanted was an in-memory representation of the XML file. NuDoq uses the visitor pattern and could be useful to me in future, but it just didn't suit my needs for this particular project.
So I looked around again and that's when I found the AutoHelp project, which made use of an older project called Jolt.NET. The solution there was to use as XSD file. Apparently, there is no official XSD schema for the XML comments file and third party ones are few and far between. Jolt.NET's implementation is not as thorough as some of the ones I have seen, but it doesn't throw any errors, because it's not so restrictive. Implementation is as follows:
="1.0"="utf-8"
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="unqualified">
<xs:simpleType name="NameType">
<xs:restriction base="xs:string">
<xs:minLength value="1"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="AssemblyType">
<xs:sequence>
<xs:element name="name" type="NameType"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="MemberType" mixed="true">
<xs:sequence>
<xs:any minOccurs="0" maxOccurs="unbounded" processContents="skip"/>
</xs:sequence>
<xs:attribute name="name" type="NameType"/>
</xs:complexType>
<xs:complexType name="MemberCollectionType">
<xs:sequence>
<xs:element name="member" type="MemberType" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="DocumentType">
<xs:sequence>
<xs:element name="assembly" type="AssemblyType"/>
<xs:element name="members" type="MemberCollectionType"/>
</xs:sequence>
</xs:complexType>
<xs:element name="doc" type="DocumentType"/>
</xs:schema>
This is then used in Jolt's XmlDocCommentReadPolicy
and XmlDocCommentReader
to parse the XML. The final piece of the puzzle is to have a class to use the aforementioned XmlDocCommentReader
to read the XML comments into an object. I won't go into all the details here, as you can obtain the code for yourself and look at DocParser.cs in the Doxie.Core
project. But essentially, it reads the XML comments file and merges that with data it obtains from reflection of the assembly and its types.
Serializing Data to File in JSON Format
The last part of this process is in using the DocParser
to acquire the data for all selected assemblies and serializing the result to file in JSON format. My implementation is as follows:
public static class JsonHelpFileGenerator
{
private static DocParser docParser = new DocParser();
public static void Generate(IEnumerable<string> selectedAssemblyPaths, string outputPath)
{
var assemblies = GetAssemblies(selectedAssemblyPaths);
string outputFileName = Path.Combine(outputPath, "assemblies.json");
assemblies.ToJson().ToFile(outputFileName);
}
private static IEnumerable<AssemblyModel> GetAssemblies(IEnumerable<string> selectedAssemblyPaths)
{
return selectedAssemblyPaths.Select(filePath => GetAssembly(filePath)).ToArray();
}
private static AssemblyModel GetAssembly(string filePath)
{
var assembly = docParser.Parse(filePath);
assembly.FileName = filePath;
return assembly;
}
}
Note: The ToJson()
and ToFile(string)
methods in the above code sample are extension methods from the Extenso project.
That's pretty much it. Once you have all these pieces in place, all you need is to create a tool to take in the arguments which are to be passed to JsonHelpFileGenerator.Generate(IEnumerable<string> selectedAssemblyPaths, string outputPath)
. Doxie provides both a GUI version and a console app for this.
Bundling
Doxie's default setup can be a little slow. You can improve this by enabling JSPM bundling. I have added the necessary configuration in packages.json and gulpfile.js. All you need to do is run gulp bundle.
Note: Gulp tends to complain about the baseURL in the config.js being set to location.pathname
. You can temporarily (or permanently if you wish) change this to the hardcoded path you need. In many cases, this can be simply / and for GitHub pages, it tends to be /[Your Project Name]/. The Gulp task should run fine then.
Further Note: You can also swap out JSPM for webpack, as previously mentioned under "Getting Started" above.
Credits
The code and XSD schema for reading the XML comments files comes from an old project named Jolt.NET. The original source code can be found here and RedGate has created their own fork on GitHub here, which had an important bug fix in it.
As for the UI and the overall idea, I was inspired by AutoHelp but totally reworked it to use Aurelia and I also decided it's better to generate a JSON file to read from instead of relying on MVC controller actions to acquire the data. This way, it's easy to use in GitHub pages.
Limitations
- XML files must be present alongside the DLLs. That is the point, but in some cases perhaps there are 1 or 2 assemblies you have not yet generated XML documentation for. It would be good if Doxie still read the types and generated some of the information without the XML comments in that case. This will be done in future.
- The generator doesn't know what to do with some XML elements (for example:
<see>
) and so it just ignores them. This may be fixed in future.