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
.
The documentation project contains several files and folders we can use to customise the generated documentation:
docfx.json
- configuration fileindex.md
- home page for the generated sitetoc.yml
- main navigation menuarticles
- folder for custom articlesapi/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:
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/ 🎉
Links
Did you find this article valuable?
Support Dave Murray by becoming a sponsor. Any amount is appreciated!