Document your .NET code with DocFX and GitHub Actions

Document your .NET code with DocFX and GitHub Actions

Dave Murray's photo
Dave Murray
·Dec 19, 2021·

8 min read

Table of contents

  • Example Code
  • Installing DocFX
  • Create a Documentation Project
  • Add XML Doc Comments
  • GitHub Actions Workflows
  • Configure GitHub Pages
  • Links

In this article I show how to use XML doc comments, DocFX and GitHub Actions to automatically build and publish documentation for your .NET code. Along the way I'll explain how to install DocFX and set up a documentation project, add XML doc comments to your code, generate Intellisense tooltips and use GitHub Actions to build and publish your documentation to GitHub Pages. The code for this article is available on GitHub: irongut/DocFXExample

Example Code

The example solution in the src folder contains a .NET Standard 2.1 class library DocFXExample and a test project. DocFXExample has one class Example which contains a single method Divide(). The code itself is not important, we just need something we can document.

namespace DocFXExample
{
    public static class Example
    {
        public static (int quotient, int remainder) Divide(int dividend, int divisor)
        {
            return (dividend / divisor, dividend % divisor);
        }
    }
}

Installing DocFX

DocFX is a documentation generator for .NET which supports C#, Visual Basic and F#. Using XML doc comments in your code along with Markdown and YAML files, DocFX builds a static HTML website which can be hosted on GitHub Pages or any web server.

DocFX can be integrated with Visual Studio but I prefer using the command line version which can be installed using Chocolatey, Homebrew or manually from GitHub.

  • Chocolatey: choco install docfx -y
  • Homebrew: brew install docfx
  • GitHub: Download DocFX, extract to a folder and add it to your PATH

Create a Documentation Project

First we need to initialise a new DocFX project. In the root of the repo type the command:

docfx init -q

This generates a documentation project named docfx_project.

DocFX Project

The documentation project contains several files and folders we can use to customise the generated documentation:

  • docfx.json - configuration file
  • index.md - home page for the generated site
  • toc.yml - main navigation menu
  • articles - folder for custom articles
  • api/index.md - index page for API documentation

We need to tell DocFX where to find our code by updating the src property in the metadata section of the configuration file docfx.json. We also need to change the output folder dest from _site to ../docs to make publishing to GitHub Pages easier:

{
  "metadata": [
    {
      "src": [
        {
          "src": "../",
          "files": [
            "src/DocFXExample/**.csproj"
          ]
        }
      ],
      "dest": "api",
      "disableGitFeatures": false,
      "disableDefaultFilter": false
    }
  ],
  "build": {
    "content": [
      {
        "files": [
          "api/**.yml",
          "api/index.md"
        ]
      },
      {
        "files": [
          "articles/**.md",
          "articles/**/toc.yml",
          "toc.yml",
          "*.md"
        ]
      }
    ],
    "resource": [
      {
        "files": [
          "images/**"
        ]
      }
    ],
    "overwrite": [
      {
        "files": [
          "apidoc/**.md"
        ],
        "exclude": [
          "obj/**",
          "_site/**"
        ]
      }
    ],
    "dest": "../docs",
    "globalMetadataFiles": [],
    "fileMetadataFiles": [],
    "template": [
      "default"
    ],
    "postProcessors": [],
    "markdownEngineName": "markdig",
    "noLangKeyword": false,
    "keepFileLink": false,
    "cleanupCacheHistory": false,
    "disableGitFeatures": false
  }
}

DocFX includes its own .gitignore files in the docfx_project and docfx_project/api folders but I like to keep my ignores central so I delete those files and add the following to the root .gitignore file:

# DocFX
docfx_project/api/.manifest
docfx_project/api/*.yml
docs

We can now build and test our documentation project locally with the following command:

docfx docfx_project\docfx.json --serve

You can view the generated website at: http://localhost:8080/

Add XML Doc Comments

XML documentation comments are special comment fields indicated by triple slashes which can be converted into documentation by tools like DocFX, Sandcastle and Doxygen.

namespace DocFXExample
{
    /// <summary>A simple example class with one method which divides two numbers.</summary>
    public static class Example
    {
        /// <summary>Divides the specified dividend by the divisor, returning the quotient and the remainder as a Tuple.</summary>
        /// <param name="dividend">The dividend - the number which will be divided by the divisor.</param>
        /// <param name="divisor">The divisor - the number by which the dividend will be divided.</param>
        /// <returns>(quotient, remainder)</returns>
        public static (int quotient, int remainder) Divide(int dividend, int divisor)
        {
            return (dividend / divisor, dividend % divisor);
        }
    }
}

Intellisense Tooltips

The C# compiler can also process XML doc comments and generate IntelliSense tooltips in Visual Studio. To enable Intellisense we need to tell the compiler to generate an XML file by adding a PropertyGroup to the csproj project file:

<PropertyGroup>
  <DocumentationFile>DocFXExample\Taranis.DocFXExample.xml</DocumentationFile>
</PropertyGroup>

After building the project the contents of the summary and returns tags will now appear in Visual Studio:

Intellisense Tooltip

GitHub Actions Workflows

In the example repo I've created three GitHub Actions workflows:

  • ci-build.yml
  • docs-only-build.yml
  • release-build.yml

All three workflows include a manual workflow_dispatch trigger for testing purposes and use Ubuntu runners. Why Ubuntu? Windows runners are charged at 2x the cost of Linux runners, MacOS runners are charged at 10x the cost of Linux runners and only Linux runners are compatible with Docker based Actions so unless you need Windows or MacOS to build your solution it is better to use an Ubuntu runner.

CI Build

ci-build.yml is a typical .NET Continuous Integration workflow with a single job that runs on any push or pull request to the master branch. It checks out the repo, sets up .NET, builds and tests the solution before uploading the Nuget library and test coverage report as build artifacts.

name: CI Build

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]
  workflow_dispatch:
permissions:
  contents: read  
  pull-requests: write

env:
  DOTNET_NOLOGO: true                     # Disable the .NET logo in the console output
  DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true # Disable the .NET first time experience to skip caching NuGet packages and speed up the build
  DOTNET_CLI_TELEMETRY_OPTOUT: true       # Disable sending .NET CLI telemetry to Microsoft

jobs:
  build:
    runs-on: ubuntu-latest
    name: CI Build
    steps:
    - name: Checkout
      uses: actions/checkout@v2

    - name: Setup .NET
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: 5.0.x

    - name: Restore Dependencies
      run: dotnet restore src/DocFXExample.sln

    - name: Build
      run: dotnet build src/DocFXExample.sln --configuration Release --no-restore

    - name: Test
      run: dotnet test src/DocFXExample.sln --configuration Release --no-build --verbosity normal --collect:"XPlat Code Coverage" --results-directory ./coverage

    - name: Copy Coverage To Predictable Location
      run: cp coverage/**/coverage.cobertura.xml coverage.cobertura.xml

    - name: Coverage Summary Report
      uses:  irongut/CodeCoverageSummary@v1.2.0
      with:
        filename: coverage.cobertura.xml
        badge: true
        format: 'md'
        output: 'both'

    - name: Add Coverage PR Comment
      uses: marocchino/sticky-pull-request-comment@v2
      if: github.event_name == 'pull_request'
      with:
        recreate: true
        path: code-coverage-results.md

    - name: Upload Coverage Artifact
      uses: actions/upload-artifact@v2.3.0
      with:
        name: test-coverage-report
        path: |
          coverage.cobertura.xml
          code-coverage-results.md

    - name: Upload Nuget Artifact
      uses: actions/upload-artifact@v2.3.0
      with:
        name: ci-nugets
        path: src/DocFXExample/bin/Release/Taranis.DocFXExample*.nupkg

Documentation Build

docs-only-build.yml is a manually triggered workflow that uses DocFX to generate and publish our documentation. It consists of a single job which checks out the repo, builds the documentation using the nikeee/docfx-action action and publishes it to GitHub Pages using the peaceiris/actions-gh-pages action. This workflow is useful for testing the documentation job and updating the site with new articles or customisation changes.

name: Docs Only Build

on:
  workflow_dispatch:

jobs:
  build-docs:
    name: Build Docs
    runs-on: ubuntu-latest
    steps:
    - name: Checkout
      uses: actions/checkout@v2

    - name: Build Documentation
      uses: nikeee/docfx-action@v1.0.0
      with:
        args: docfx_project/docfx.json

    - name: Deploy to GitHub Pages
      uses: peaceiris/actions-gh-pages@v3
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        publish_dir: ./docs

Release Build

release-build.yml is where we put everything together. Triggered whenever a pre-release or full release is published it consists of three jobs that run sequentially.

The first job build is a slightly simplified version of our CI Build workflow. It checks out the repo, builds and tests the code and uploads the Nuget and test coverage report as an artifact.

The deploy-nuget job only runs if the build job was successful. It gets the artifact from the previous step, publishes the Nuget package and adds the test coverage report to the release notes. Or it would in a real project, since this is just an example I've left out the steps to deploy the package to nuget.org, GitHub Packages or another package feed.

The final job deploy-docs is a copy of our Documentation Build workflow. It will only run if both the build and deploy-nuget jobs are successful. Like the previous workflow it checks out the repo, builds the documentation and publishes it to GitHub Pages.

name: Build + Deploy

on:
  release:
    types: [published]
    branches: [master]
  workflow_dispatch:

env:
  DOTNET_NOLOGO: true                     # Disable the .NET logo
  DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true # Disable the .NET first time experience to speed up the build
  DOTNET_CLI_TELEMETRY_OPTOUT: true       # Disable sending .NET CLI telemetry

jobs:
  build:
    runs-on: ubuntu-latest
    name: Release Build
    steps:
    - name: Checkout
      uses: actions/checkout@v2

    - name: Setup .NET
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: 5.0.x

    - name: Restore Dependencies
      run: dotnet restore src/DocFXExample.sln

    - name: Build
      run: dotnet build src/DocFXExample.sln --configuration Release --no-restore

    - name: Test
      run: dotnet test src/DocFXExample.sln --configuration Release --no-build --verbosity normal --collect:"XPlat Code Coverage" --results-directory ./coverage

    - name: Copy Coverage To Predictable Location
      run: cp coverage/**/coverage.cobertura.xml coverage.cobertura.xml

    - name: Coverage Summary Report
      uses: irongut/CodeCoverageSummary@v1.2.0
      with:
        filename: coverage.cobertura.xml
        badge: true
        format: 'md'
        output: 'both'

    - name: Upload Coverage Artifact
      uses: actions/upload-artifact@v2.3.0
      with:
        name: release-nugets
        path: code-coverage-results.md

    - name: Upload Nuget Artifact
      uses: actions/upload-artifact@v2.3.0
      with:
        name: release-nugets
        path: src/DocFXExample/bin/Release/Taranis.DocFXExample*.nupkg

  deploy-nuget:
    name: Deploy Nuget
    needs: [build]
    runs-on: ubuntu-latest
    steps:
    - name: Download Artifacts
      uses: actions/download-artifact@v2
      with:
        name: release-nugets

    # Here you can deploy your Nuget package to 
    # nuget.org, GitHub Packages or a private package feed

    - name: Add Coverage to Release
      uses: irongut/EditRelease@v1.0.0
      with:
        token: ${{ secrets.GITHUB_TOKEN }}
        id: ${{ github.event.release.id }}
        files: code-coverage-results.md

  deploy-docs:
    name: Deploy Docs
    needs: [build, deploy-nuget]
    runs-on: ubuntu-latest
    steps:
    - name: Checkout
      uses: actions/checkout@v2

    - name: Build Documentation
      uses: nikeee/docfx-action@v1.0.0
      with:
        args: docfx_project/docfx.json

    - name: Deploy to GitHub Pages
      uses: peaceiris/actions-gh-pages@v3
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        publish_dir: ./docs

Configure GitHub Pages

After we publish our documentation for the first time, using either the docs only or the release workflow, we need to configure GitHub Pages to show it. On the Settings tab for the repo click Pages in the left menu. Our workflows publish the documentation to a branch called gh-pages. Select the gh-pages branch in the first drop down, / (root) in the second drop down and click Save. Our documentation site is now available on the url shown, in this case: https://irongut.github.io/DocFXExample/ 🎉

configure-github-pages.webp

 
Share this