107

The documentation for enabling XmlDoc integration into your Web Api projects appears to only handle situations where all of your API types are part of your WebApi project. In particular, it discusses how to reroute the XML documentation to App_Data/XmlDocument.xml and uncommenting a line in your config that will consume that file. This implicitly only allows for one project's documentation file.

However, in my setup I have my request and response types defined in a common "Models" project. This means that if I have an endpoint defined such as:

[Route("auth/openid/login")]
public async Task<AuthenticationResponse> Login(OpenIdLoginRequest request) { ... }

Where OpenIdLoginRequest is defined in a separate C# project like so:

public class OpenIdLoginRequest
{
    /// <summary>
    /// Represents the OpenId provider that authenticated the user. (i.e. Facebook, Google, etc.)
    /// </summary>
    [Required]
    public string Provider { get; set; }

    ...
}

Despite the XML doccomments, the properties of the request parameter contain no documentation when you view the endpoint-specific help page (i.e. http://localhost/Help/Api/POST-auth-openid-login).

How can I make it so that types in subprojects with XML documentation are surfaced in the Web API XML documentation?

Kirk Woll
  • 76,112
  • 22
  • 180
  • 195

5 Answers5

170

There is no built-in way to achieve this. However, it requires only a few steps:

  1. Enable XML documentation for your subproject (from project properties / build) like you have for your Web API project. Except this time, route it directly to XmlDocument.xml so that it gets generated in your project's root folder.

  2. Modify your Web API project's postbuild event to copy this XML file into your App_Data folder:

    copy "$(SolutionDir)SubProject\XmlDocument.xml" "$(ProjectDir)\App_Data\Subproject.xml"
    

    Where Subproject.xml should be renamed to whatever your project's name is plus .xml.

  3. Next open Areas\HelpPage\App_Start\HelpPageConfig and locate the following line:

    config.SetDocumentationProvider(new XmlDocumentationProvider(
        HttpContext.Current.Server.MapPath("~/App_Data/XmlDocument.xml")));
    

    This is the line you initially uncommented in order to enable XML help documentation in the first place. Replace that line with:

    config.SetDocumentationProvider(new XmlDocumentationProvider(
        HttpContext.Current.Server.MapPath("~/App_Data")));
    

    This step ensures that XmlDocumentationProvider is passed the directory that contains your XML files, rather than the specific XML file for your project.

  4. Finally, modify Areas\HelpPage\XmlDocumentationProvider in the following ways:

    a. Replace the _documentNavigator field with:

    private List<XPathNavigator> _documentNavigators = new List<XPathNavigator>();
    

    b. Replace the constructor with:

    public XmlDocumentationProvider(string appDataPath)
    {
        if (appDataPath == null)
        {
            throw new ArgumentNullException("appDataPath");
        }
    
        var files = new[] { "XmlDocument.xml", "Subproject.xml" };
        foreach (var file in files)
        {
            XPathDocument xpath = new XPathDocument(Path.Combine(appDataPath, file));
            _documentNavigators.Add(xpath.CreateNavigator());
        }
    }
    

    c. Add the following method below the constructor:

    private XPathNavigator SelectSingleNode(string selectExpression)
    {
        foreach (var navigator in _documentNavigators)
        {
            var propertyNode = navigator.SelectSingleNode(selectExpression);
            if (propertyNode != null)
                return propertyNode;
        }
        return null;
    }
    

    d. And last, fix all compiler errors (there should be three) resulting in references to _documentNavigator.SelectSingleNode and remove the _documentNavigator. portion so that it now calls the new SelectSingleNode method we defined above.

This Last step is what modifies the document provider to support looking within multiple XML documents for the help text rather than just the primary project's.

Now when you examine your Help documentation, it will include XML documentation from types in your related project.

Pishang Ujeniya
  • 176
  • 5
  • 13
Kirk Woll
  • 76,112
  • 22
  • 180
  • 195
  • 8
    Excellent answer. I actually think it's a little easier for the constructor to accept an array of strings: public XmlDocumentationProvider(string appDataPath) and enumerate this list in the Documentation provider. – Captain John Feb 20 '14 at 16:55
  • 1
    @CaptainJohn, thanks! I agree your suggestion would work well. However, I think I'll leave it as it is as it keeps most of the modifications in `XmlDocumentationProvider`. I was initially going to leave that line unchanged and extract the parent folder from the passed-in file, but that seemed too kludgey to me. – Kirk Woll Feb 20 '14 at 23:54
  • 15
    Fantastic, this was just what I was looking for!! Suggest replacing the `var files...` line with `var files = Directory.GetFiles(documentPath, "*.xml");` if you (like me) won't always know the names / number of xml documentation files that will be there. Could also do futher filtering as needed. – sǝɯɐſ Jul 03 '14 at 19:25
  • Also, I only had to fix 2 references in step 4d... I'm using Web API 2.1 – sǝɯɐſ Jul 03 '14 at 19:53
  • 1
    Any reason to not just set the XML documentation file to "App_Data\XmlDocument.xml" and skip the build step? – Steven Berkovitz Dec 15 '14 at 21:29
  • 1
    @StevenBerkovitz, but then you would blow away any XML documentation for your main Web API project, right? – Kirk Woll Dec 15 '14 at 23:54
  • 1
    Just because I got an error in the postbuild event, I thought it could be useful to someone who is having the same issue: copy "$(SolutionDir)SubProject\XmlDocument.xml" "$(ProjectDir)App_Data\SubProject.xml" – Stefano Magistri Jul 16 '15 at 13:31
  • Simply wonderful!!! Follow the steps and works like a charm!!! Thank you @KirkWoll – Rashmi Pandit Feb 03 '16 at 02:49
  • 2
    I would like to add my modifications on top of some of the others here. I used the ...\ notation to have the xml file created in the root project App_Data\documentation folder. I then used @sǝɯɐſ method of puling all xml files from that directory. This works beautifully and am surprised that this isn't just how it works out of the box. Many thanks. – Darroll Jun 17 '16 at 16:18
  • An excellent answer! I would suggest one small improvement. Let all your documents end with XmlDocument.xml (e.g.: Subproject.XmlDocument.xml) and replace `var files = new[] { "XmlDocument.xml", "Subproject.xml" }; foreach (var file in files)` with `foreach (var file in Directory.EnumerateFiles(documentPath, "*XmlDocument.xml"))` and remove Path.Combine call – Stanislav Jul 08 '16 at 12:15
  • 1
    ... speechless, almost crying... Friday 7pm followed this in total-dumb-mode without understanding a word. A CHARM – Shockwaver Jan 13 '17 at 17:05
  • Maybe I'm being dense, but why not just copy all the .xml files out of the \bin directory after build? That way you get all the documentation for all the .dlls? – DaleyKD Feb 09 '18 at 19:27
32

I ran into this too, but I didn't want to edit or duplicate any of the generated code to avoid problems later.

Building on the other answers, here's a self-contained documentation provider for multiple XML sources. Just drop this into your project:

/// <summary>A custom <see cref="IDocumentationProvider"/> that reads the API documentation from a collection of XML documentation files.</summary>
public class MultiXmlDocumentationProvider : IDocumentationProvider, IModelDocumentationProvider
{
    /*********
    ** Properties
    *********/
    /// <summary>The internal documentation providers for specific files.</summary>
    private readonly XmlDocumentationProvider[] Providers;


    /*********
    ** Public methods
    *********/
    /// <summary>Construct an instance.</summary>
    /// <param name="paths">The physical paths to the XML documents.</param>
    public MultiXmlDocumentationProvider(params string[] paths)
    {
        this.Providers = paths.Select(p => new XmlDocumentationProvider(p)).ToArray();
    }

    /// <summary>Gets the documentation for a subject.</summary>
    /// <param name="subject">The subject to document.</param>
    public string GetDocumentation(MemberInfo subject)
    {
        return this.GetFirstMatch(p => p.GetDocumentation(subject));
    }

    /// <summary>Gets the documentation for a subject.</summary>
    /// <param name="subject">The subject to document.</param>
    public string GetDocumentation(Type subject)
    {
        return this.GetFirstMatch(p => p.GetDocumentation(subject));
    }

    /// <summary>Gets the documentation for a subject.</summary>
    /// <param name="subject">The subject to document.</param>
    public string GetDocumentation(HttpControllerDescriptor subject)
    {
        return this.GetFirstMatch(p => p.GetDocumentation(subject));
    }

    /// <summary>Gets the documentation for a subject.</summary>
    /// <param name="subject">The subject to document.</param>
    public string GetDocumentation(HttpActionDescriptor subject)
    {
        return this.GetFirstMatch(p => p.GetDocumentation(subject));
    }

    /// <summary>Gets the documentation for a subject.</summary>
    /// <param name="subject">The subject to document.</param>
    public string GetDocumentation(HttpParameterDescriptor subject)
    {
        return this.GetFirstMatch(p => p.GetDocumentation(subject));
    }

    /// <summary>Gets the documentation for a subject.</summary>
    /// <param name="subject">The subject to document.</param>
    public string GetResponseDocumentation(HttpActionDescriptor subject)
    {
        return this.GetFirstMatch(p => p.GetResponseDocumentation(subject));
    }


    /*********
    ** Private methods
    *********/
    /// <summary>Get the first valid result from the collection of XML documentation providers.</summary>
    /// <param name="expr">The method to invoke.</param>
    private string GetFirstMatch(Func<XmlDocumentationProvider, string> expr)
    {
        return this.Providers
            .Select(expr)
            .FirstOrDefault(p => !String.IsNullOrWhiteSpace(p));
    }
}

...and enable it in your HelpPageConfig with the paths to the XML documents you want:

config.SetDocumentationProvider(new MultiXmlDocumentationProvider(HttpContext.Current.Server.MapPath("~/App_Data/Api.xml"), HttpContext.Current.Server.MapPath("~/App_Data/Api.Models.xml")));
Pathoschild
  • 4,636
  • 2
  • 23
  • 25
  • This is a great solution. I prefer it over solutions that require modification of the default HelpPage classes as they will be overwritten on updates. – AronVanAmmers Apr 15 '15 at 20:06
  • 3
    This works brilliantly, thank you for posting. To save anyone using this a bit of time, you still need to do the first two stages of kirk's accepted answer above, i.e. 1) Enable XML documentation for your subproject and 2) Modify your Web API project's postbuild event to copy this XML file into your App_Data folder. – tomRedox Jul 01 '15 at 11:51
  • 1
    and then this line becomes: config.SetDocumentationProvider(new MultiXmlDocumentationProvider(HttpContext.Current.Server.MapPath("~/App_Data/[original web api project's xml filename, defaults to XmlDocument].xml"), HttpContext.Current.Server.MapPath("~/App_Data/[Whatever you called your SubProject xml filename].xml"))); – tomRedox Jul 01 '15 at 11:54
  • Followed the steps but didn't work:(. There is no error so not sure what went wrong. It still only shows the api document but not the additional project document. I also tried the steps in the accepted answer and it's the same thing. Anything in particular I should check? – dragonfly02 Nov 19 '15 at 00:33
  • For some reason I still see the default GET api/me that comes with the getting started project template in VS. – John Zabroski Apr 21 '16 at 14:18
  • noticed one minor thing: the `GetResponseDocumentation` method should return `this.GetFirstMatch(p => p.GetResponseDocumentation(subject));` – mikelt21 Apr 12 '18 at 13:14
  • The first attempt working fine. Thanks for the solution. – Mahi Mar 04 '20 at 05:27
5

One more simplified way of doing this is by merging the xml files. Example code in my below reply:

Web Api Help Page XML comments from more than 1 files

Community
  • 1
  • 1
Kiran
  • 56,921
  • 15
  • 176
  • 161
0

Easiest way to fix this issue is creating the App_Code folder on server you deployed. Then copy the XmlDocument.xml you have in your bin folder locally into the App_Code folder

Ziregbe Otee
  • 534
  • 3
  • 9
  • Thanks for suggestion!! No more -1 for such helpful answer. Yes, if you deploying it to Azure Cloud App Service, many issues occur with multiple *.xml deploying, so making them available for swagger, for example, may be really tricky. But I would rather choose another standard ASP.Net server-side folder, namely App_GlobalResources, since xmldoc files are pretty much similar to resources. It is especially true because I still did not have App_Code folder in my project and it did not matter which standard folder to create. – moudrick Nov 16 '16 at 16:20
  • The following standard folder worked for me: App_Code - is not visible from client on default settings App_GlobalResources - is not visible from client on default settings App_LocalResources - is not visible from client on default settings – moudrick Nov 16 '16 at 16:20
  • Let me also list out the issues with each of standard folders that did not work for me. bin - only *.xml for main assembly is deplopyed to App_Data - the most practical setting is to skip everything in this folder on deploying to cloud – moudrick Nov 16 '16 at 16:20
  • Could anyone who interested edit this answer to reflect all these considerations above, probably with extended speculations? – moudrick Nov 16 '16 at 16:20
0

I found a better solution

  1. Go to properties of your solution and on Built, Out Put, Documentation XML File just fill with your folder on your app data.

  2. Add a line with the file you want to insert into your documentation like this.

config.SetDocumentationProvider(new XmlDocumentationProvider( HttpContext.Current.Server.MapPath("~/App_Data/FenixCorporate.API.xml")));

        config.SetDocumentationProvider(new XmlDocumentationProvider(
            HttpContext.Current.Server.MapPath("~/App_Data/FenixCorporate.Entities.xml")));
user1768874
  • 61
  • 1
  • 2