Programmatically get Summary comments at runtime
Asked Answered
C

7

81

I'm looking for a way to programmatically get the summary portion of Xml-comments of a method in ASP.net.

I have looked at the previous related posts and they do not supply a way of doing so in a web environment.

I can not use any 3rd party apps and due to a web environment, Visual studio plugin's aren't much use either.

The closest thing I have found to a working solution was the JimBlackler project, but it only works on DLL's.

Naturally, something like 'supply .CS file, get XML documentation' would be optimal.


Current situation

I have a web-service and trying to dynamically generate documentation for it.

Reading the Methods, and properties is easy, but getting the Summary for each method is throwing me off a bit.

/// <summary>
/// This Is what I'm trying to read
/// </summary>
public class SomeClass()
{
    /// <summary>
    /// This Is what I'm trying to read
    /// </summary>
    public void SomeMethod()
    {
    }
}

Chappell answered 24/3, 2013 at 18:57 Comment(5)
What exactly is it you're trying to accomplish? What's the "summary of a method"? Why couldn't you use third-party code in a web environment, I use a whole mess of it? Which Jim Blackler project? Why is it a problem that it only works on DLLs? That's how .NET web apps get deployed. It seems like you're assuming we can read your mind, you have to actually clarify what you're talking about and what you've researched and tried.Engel
Ill try clarify, will update the post in a sec.Chappell
millimoose, which 3rd party tools do you use for this purpose, i couldnt find anything efficient?Chappell
If you have access to the .cs file you're trying to read, you can use CodeDOM to compile it and extract the XML doc comments. MSDN even has a howto that demonstrates some the APIs: msdn.microsoft.com/en-us/library/ms404261.aspx (It goes the other way around though, by generating code at runtime, but it should be possible to somehow feed a CS file to CodeDOM.)Engel
I've changed title - feel free to revert if you disagree.Erastianism
A
43

The XML summary isn't stored in the .NET assembly - it's optionally written out to an XML file as part of your build (assuming you're using Visual Studio).

Consequently there is no way to "pull out" the XML summaries of each method via reflection on a compiled .NET assembly (either .EXE or .DLL) - because the data simply isn't there for you to pull out. If you want the data, you'll have to instruct your build environment to output the XML files as part of your build process and parse those XML files at runtime to get at the summary information.

Arabesque answered 24/3, 2013 at 19:12 Comment(1)
How do you "instruct your build environment to output the XML files as part of your build process" ?Metz
D
52

A Workaround - Using reflection on Program.DLL/EXE together with Program.XML file

If you take a look at the sibling .XML file generated by Visual Studio you will see that there is a fairly flat hierarchy of /members/member. All you have to do is get hold on each method from your DLL via MethodInfo object. Once you have this object you turn to the XML and use XPATH to get the member containing the XML documentation for this method.

Members are preceded by a letter. XML doc for methods are preceded by "M:" for class by "T:" etc.

Load your sibling XML

string docuPath = dllPath.Substring(0, dllPath.LastIndexOf(".")) + ".XML";

if (File.Exists(docuPath))
{
  _docuDoc = new XmlDocument();
  _docuDoc.Load(docuPath);
}

Use this xpath to get the member representing the method XML docu

string path = "M:" + mi.DeclaringType.FullName + "." + mi.Name;

XmlNode xmlDocuOfMethod = _docuDoc.SelectSingleNode(
    "//member[starts-with(@name, '" + path + "')]");

Now scan childnodes for all the rows of "///" Sometimes the /// Summary contains extra blanks, if this bothers use this to remove

var cleanStr = Regex.Replace(row.InnerXml, @"\s+", " ");
Dobruja answered 26/12, 2013 at 19:49 Comment(1)
This is great, although you can greatly simplify your first line by using Path.ChangeExtension(dllPath, ".XML")Ipomoea
A
43

The XML summary isn't stored in the .NET assembly - it's optionally written out to an XML file as part of your build (assuming you're using Visual Studio).

Consequently there is no way to "pull out" the XML summaries of each method via reflection on a compiled .NET assembly (either .EXE or .DLL) - because the data simply isn't there for you to pull out. If you want the data, you'll have to instruct your build environment to output the XML files as part of your build process and parse those XML files at runtime to get at the summary information.

Arabesque answered 24/3, 2013 at 19:12 Comment(1)
How do you "instruct your build environment to output the XML files as part of your build process" ?Metz
T
31

You could 'document' your method using the System.ComponentModel.DataAnnotations.DisplayAttribute attribute, e.g.

[Display(Name = "Foo", Description = "Blah")]
void Foo()
{
}

then use reflection to pull the description at runtime.

Torpid answered 24/3, 2013 at 22:32 Comment(3)
+1. This approach looks great for simplicity using system reflection and getting what we need at run time. We could even do custom attributes for documentation if needed as well and read our documentation in a web api service for the consumers. Very very nice approach!Trot
I agree @revobtz, it may be better to create a custom attribute for this, as the documentation for the System.ComponentModel.DataAnnotations namespace states "...provides attribute classes that are used to define metadata for ASP.NET MVC and ASP.NET data controls". Therefore it's intended for UI use rather than documentation.Torpid
How to use reflection to pull Description? I have no ideaPharisaic
P
27

A deleted post, made by @OleksandrIeremenko, on this thread links to this article https://jimblackler.net/blog/?p=49 which was the basis for my solution.

Below is a modification of Jim Blackler's code making extension methods off the MemberInfo and Type objects and adding code that returns the summary text or an empty string if not available.

Usage

var typeSummary = typeof([Type Name]).GetSummary();
var methodSummary = typeof([Type Name]).GetMethod("[Method Name]").GetSummary();

Extension Class

/// <summary>
/// Utility class to provide documentation for various types where available with the assembly
/// </summary>
public static class DocumentationExtensions
{
    /// <summary>
    /// Provides the documentation comments for a specific method
    /// </summary>
    /// <param name="methodInfo">The MethodInfo (reflection data ) of the member to find documentation for</param>
    /// <returns>The XML fragment describing the method</returns>
    public static XmlElement GetDocumentation(this MethodInfo methodInfo)
    {
        // Calculate the parameter string as this is in the member name in the XML
        var parametersString = "";
        foreach (var parameterInfo in methodInfo.GetParameters())
        {
            if (parametersString.Length > 0)
            {
                parametersString += ",";
            }

            parametersString += parameterInfo.ParameterType.FullName;
        }

        //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);
    }

    /// <summary>
    /// Provides the documentation comments for a specific member
    /// </summary>
    /// <param name="memberInfo">The MemberInfo (reflection data) or the member to find documentation for</param>
    /// <returns>The XML fragment describing the member</returns>
    public static XmlElement GetDocumentation(this MemberInfo memberInfo)
    {
        // First character [0] of member type is prefix character in the name in the XML
        return XmlFromName(memberInfo.DeclaringType, memberInfo.MemberType.ToString()[0], memberInfo.Name);
    }
    /// <summary>
    /// Returns the Xml documenation summary comment for this member
    /// </summary>
    /// <param name="memberInfo"></param>
    /// <returns></returns>
    public static string GetSummary(this MemberInfo memberInfo)
    {
        var element = memberInfo.GetDocumentation();
        var summaryElm = element?.SelectSingleNode("summary");
        if (summaryElm == null) return "";
        return summaryElm.InnerText.Trim();
    }

    /// <summary>
    /// Provides the documentation comments for a specific type
    /// </summary>
    /// <param name="type">Type to find the documentation for</param>
    /// <returns>The XML fragment that describes the type</returns>
    public static XmlElement GetDocumentation(this Type type)
    {
        // Prefix in type names is T
        return XmlFromName(type, 'T', "");
    }

    /// <summary>
    /// Gets the summary portion of a type's documenation or returns an empty string if not available
    /// </summary>
    /// <param name="type"></param>
    /// <returns></returns>
    public static string GetSummary(this Type type)
    {
        var element = type.GetDocumentation();
        var summaryElm = element?.SelectSingleNode("summary");
        if (summaryElm == null) return "";
        return summaryElm.InnerText.Trim();
    }

    /// <summary>
    /// Obtains the XML Element that describes a reflection element by searching the 
    /// members for a member that has a name that describes the element.
    /// </summary>
    /// <param name="type">The type or parent type, used to fetch the assembly</param>
    /// <param name="prefix">The prefix as seen in the name attribute in the documentation XML</param>
    /// <param name="name">Where relevant, the full name qualifier for the element</param>
    /// <returns>The member that has a name that describes the specified reflection element</returns>
    private static XmlElement XmlFromName(this Type type, char prefix, string name)
    {
        string fullName;

        if (string.IsNullOrEmpty(name))
            fullName = prefix + ":" + type.FullName;
        else
            fullName = prefix + ":" + type.FullName + "." + name;

        var xmlDocument = XmlFromAssembly(type.Assembly);

        var matchedElement = xmlDocument["doc"]["members"].SelectSingleNode("member[@name='" + fullName + "']") as XmlElement;

        return matchedElement;
    }

    /// <summary>
    /// A cache used to remember Xml documentation for assemblies
    /// </summary>
    private static readonly Dictionary<Assembly, XmlDocument> Cache = new Dictionary<Assembly, XmlDocument>();

    /// <summary>
    /// A cache used to store failure exceptions for assembly lookups
    /// </summary>
    private static readonly Dictionary<Assembly, Exception> FailCache = new Dictionary<Assembly, Exception>();

    /// <summary>
    /// Obtains the documentation file for the specified assembly
    /// </summary>
    /// <param name="assembly">The assembly to find the XML document for</param>
    /// <returns>The XML document</returns>
    /// <remarks>This version uses a cache to preserve the assemblies, so that 
    /// the XML file is not loaded and parsed on every single lookup</remarks>
    public static XmlDocument XmlFromAssembly(this Assembly assembly)
    {
        if (FailCache.ContainsKey(assembly))
        {
            throw FailCache[assembly];
        }

        try
        {

            if (!Cache.ContainsKey(assembly))
            {
                // load the docuemnt into the cache
                Cache[assembly] = XmlFromAssemblyNonCached(assembly);
            }

            return Cache[assembly];
        }
        catch (Exception exception)
        {
            FailCache[assembly] = exception;
            throw;
        }
    }

    /// <summary>
    /// Loads and parses the documentation file for the specified assembly
    /// </summary>
    /// <param name="assembly">The assembly to find the XML document for</param>
    /// <returns>The XML document</returns>
    private static XmlDocument XmlFromAssemblyNonCached(Assembly assembly)
    {
        var assemblyFilename = assembly.Location;
   
        if (!string.IsNullOrEmpty(assemblyFilename))
        {
            StreamReader streamReader;

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

            var xmlDocument = new XmlDocument();
            xmlDocument.Load(streamReader);
            return xmlDocument;
        }
        else
        {
            throw new Exception("Could not ascertain assembly filename", null);
        }
    }
}
Paxton answered 2/1, 2019 at 15:54 Comment(2)
In VisualStudio, generating document xml file is disabled by default. However, we can enable it: Right click on the project you want to enable xml-document generating > properties > Build tab > check the "Xml documentation file" check-box. OR simply set the '<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <DocumentationFile>AddressAndNameOfYourProject.xml</DocumentationFile> </PropertyGroup>` in YourProject.csproj file.Diep
any API or nuget package for manage csproj files?Capet
F
9

You can use Namotion.Reflection NuGet package to get these information:

string summary = typeof(Foo).GetXmlDocsSummary();
Forced answered 20/1, 2020 at 14:59 Comment(0)
T
1

You can look at https://github.com/NSwag/NSwag - source for nuget NSwag.CodeGeneration - it gets summary as well, usage

var generator = new WebApiAssemblyToSwaggerGenerator(settings);<br/>
var swaggerService = generator.GenerateForController("namespace.someController");<br/>
// string with comments <br/>
var swaggerJson = swaggerService.ToJson(); 

(try ILSPY decompiler against your dll, you check code and comments)

Tjader answered 10/9, 2016 at 2:54 Comment(2)
Thank you - worked for me like a charm. In my case I have adapted this reflection.Arenas
NSwag.CodeGeneration available in 2023?Capet
M
0

If you have access to the source code you're trying to get comments for, then you can use Roslyn compiler platform to do that. It basically gives you access to all the intermediary compiler metadata and you can do anything you want with it.

It's a bit more complicated than what other people are suggesting, but depending on what your needs are, might be an option.

It looks like this post has a code sample for something similar.

Maurreen answered 4/8, 2015 at 21:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.