This MSBuild Task automatically replaces <inheritdoc />
tags in your .NET XML documentation with the actual inherited docs.
-
Add some
<inheritdoc />
tags to your XML documentation comments.This tool’s handling of
<inheritdoc />
tags is based on the design document used for Roslyn's support in Visual Studio, which is in turn based on the<inheritdoc />
support in Sandcastle Help File Builder (SHFB). -
Add the SauceControl.InheritDoc NuGet package reference to your project.
This is a development-only dependency; it will not be deployed with or referenced by your compiled app/library.
-
Build your project as you normally would.
The XML docs will be post-processed automatically with each non-debug build, whether you use Visual Studio, dotnet CLI, or anything else that hosts the MSBuild engine.
-
Updates the contents of inherited docs to replace
param
andtypeparam
names that changed in the inheriting type or member. -
Supports trimming your published XML doc files of any types or members not publicly visible in your API.
-
Validates your usage of
<inheritdoc />
and warns you if no documentation exists or if yourcref
s orpath
s are incorrect.
The InheritDoc task inserts itself between the CoreCompile
and CopyFilesToOutputDirectory
steps in the MSBuild process, making a backup copy of the documentation file output from the compiler and then processing it to replace <inheritdoc />
tags. It uses the arguments passed to the compiler to find your assembly, the XML doc file, and all referenced assemblies. The output of InheritDoc is then used for the remainder of your build process. The XML documentation in your output (bin) folder will be the processed version. If you have further steps, such as building a NuGet package, the updated XML file will used in place of the original, meaning <inheritdoc />
Just Works™.
This enhances the new support for <inheritdoc />
in Roslyn (available starting in VS 16.4), making it available to all downstream consumers of your documentation. When using tools such as DocFX, you will no longer be subject to limitations around <inheritdoc />
tag usage because the documentation will already have those tags replaced with the upstream docs.
Click to expand samples:
Basic <inheritdoc />
Usage (Automatic Inheritance)
Consider the following C#
/// <summary>Interface IX</summary>
public interface IX
{
/// <summary>Method X</summary>
void X();
}
/// <inheritdoc />
public interface IY : IX
{
/// <summary>Method Y</summary>
void Y();
}
/// <summary>Class A</summary>
public class A : IY
{
void IX.X() { }
/// <inheritdoc />
public virtual void Y() { }
/// <summary>Method M</summary>
/// <typeparam name="T">TypeParam T</typeparam>
/// <param name="t">Param t</param>
/// <returns>
/// Returns value <paramref name="t" />
/// of type <typeparamref name="T" />
/// </returns>
public virtual T M<T>(T t) => t;
/// <summary>Method P</summary>
private void P() { }
/// <summary>Overloaded Method O</summary>
/// <param name="s">Param s</param>
/// <param name="t">Param t</param>
/// <param name="u">Param u</param>
public static void O(string[] s, string t, string u) { }
/// <inheritdoc cref="O(string[], string, string)" />
public static void O(string[] s) { }
}
/// <inheritdoc />
public class B : A
{
/// <inheritdoc />
public override void Y() { }
/// <inheritdoc />
public override TValue M<TValue>(TValue value) => value;
}
Once processed, the output XML documentation will look like this (results abbreviated and comments added manually to highlight features)
<member name="T:IX">
<summary>Interface IX</summary>
</member>
<member name="M:IX.X">
<summary>Method X</summary>
</member>
<member name="T:IY">
<summary>Interface IX</summary> <!-- inherited from IX -->
</member>
<member name="M:IY.Y">
<summary>Method Y</summary>
</member>
<member name="T:A">
<summary>Class A</summary>
</member>
<member name="M:A.Y">
<summary>Method Y</summary> <!-- inherited from IY -->
</member>
<member name="M:A.M``1(``0)">
<summary>Method M</summary>
<typeparam name="T">TypeParam T</typeparam>
<param name="t">Param t</param>
<returns>
Return value <paramref name="t" />
of type <typeparamref name="T" />
</returns>
</member>
<!-- private method A.P doc removed -->
<member name="M:A.O(System.String[],System.String,System.String)">
<summary>Overloaded Method O</summary>
<param name="s">Param s</param>
<param name="t">Param t</param>
<param name="u">Param u</param>
</member>
<member name="M:A.O(System.String[])">
<summary>Overloaded Method O</summary> <!-- inherited (by cref) from overload -->
<param name="s">Param s</param>
<!-- unused parameters automatically removed -->
</member>
<member name="T:B">
<summary>Class A</summary> <!-- inherited from A -->
</member>
<member name="M:B.Y">
<summary>Method Y</summary> <!-- inherited from IY (recursively through A) -->
</member>
<member name="M:B.M``1(``0)">
<summary>Method M</summary> <!-- inherited from A -->
<typeparam name="TValue">TypeParam T</typeparam> <!-- typeparam updated to match override's name -->
<param name="value">Param t</param> <!-- param updated to match override's name -->
<returns>
Returns value <paramref name="value" /> <!-- paramref and typeparamref updated as well -->
of type <typeparamref name="TValue" />
</returns>
</member>
<member name="M:A.IX#X"> <!-- explicit interface implementation doc added automatically -->
<summary>Method X</summary>
</member>
Explicit Inheritance
InheritDoc also supports the path
attribute defined in the Roslyn draft design doc, which is analogous to the select
attribute in SHFB.
In this example, we define a custom Exception class that for some reason doesn't inherit from System.Exception
and yet we want to use its documentation anyway.
public class ExceptionForSomeReasonNotInheritedFromSystemException
{
/// <inheritdoc cref="Exception(string)" />
/// <param name="theErrorMessage"><inheritdoc cref="Exception(string)" path="/param[@name='message']/node()" /></param>
ExceptionForSomeReasonNotInheritedFromSystemException(string theErrorMessage) { }
}
Outputs:
<member name="M:ExceptionForSomeReasonNotInheritedFromSystemException.#ctor(System.String)">
<summary>Initializes a new instance of the <see cref="T:System.Exception"></see> class with a specified error message.</summary>
<param name="theErrorMessage">The message that describes the error.</param>
</member>
Notice the param
element for message
was excluded automatically because there was no matching parameter on the target constructor, however with a nested <inheritdoc />
and a custom selector, we were able to extract the contents from that param
element into a new one with the correct name.
Namespace Documentation
Although the .NET compilers don't allow adding namespace documentation comments, some tools (including SHFB) have a convention for declaring them in code. InheritDoc follows this convention.
Note that both the [CompilerGenerated]
attribute and the class name NamespaceDoc
are required by InheritDoc.
namespace InheritDocTest
{
/// <summary>Namespace InheritDocTest</summary>
[CompilerGenerated] internal class NamespaceDoc { }
}
Will output:
<member name="N:InheritDocTest">
<summary>Namespace InheritDocTest</summary>
</member>
InheritDoc is enabled by default for all non-debug builds. It can be enabled or disabled explicitly by setting the InheritDocEnabled
MSBuild property in your project.
<PropertyGroup>
<InheritDocEnabled>false</InheritDocEnabled>
</PropertyGroup>
Alternatively, you can conditionally include the NuGet package only for specific configurations.
<ItemGroup Condition="'$(Configuration)'=='Dist'">
<PackageReference Include="SauceControl.InheritDoc" Version="1.*" PrivateAssets="all" />
</ItemGroup>
*NuGet tools may include a more verbose version of the PackageReference
tag when you add the package to your project. The above example is all that's actually necessary.
By default, InheritDoc will remove documentation for any types/members that are not part of the assembly's public API from the output XML. This behavior can be configured by setting the InheritDocTrimLevel
property to one of: none
, private
, or internal
. Docs belonging to types/members with API visibility at or below the InheritDocTrimLevel
will be removed. The default setting is internal
.
If your internal types/members are available to other assemblies (by means of InternalsVisibleToAttribute
) and those projects are not part of the same Visual Studio solution, you may wish to preserve the internal member docs by setting InheritDocTrimLevel
to private
.
<PropertyGroup>
<InheritDocTrimLevel>private</InheritDocTrimLevel>
</PropertyGroup>
InheritDoc will automatically discover XML documentation files alongside assemblies referenced by your project. If necessary, additional XML documentation files can be manually included with InheritDocReference
. For example:
<ItemGroup>
<InheritDocReference Include="\path\to\somedocs.xml" />
<InheritDocReference Include="\path\to\moredocs.xml" />
</ItemGroup>
If you are multi-targeting using the new(er) SDK-style projects and the TargetFrameworks
property, you must ensure that you are not generating multiple XML documentation outputs to the same file path.
If you configure the XML documentation output from the project property page in Visual Studio, you may end up with something like:
<PropertyGroup>
<DocumentationFile>MyProject.xml</DocumentationFile> <!-- NOOOOOOO! -->
</PropertyGroup>
The above configuration will create a single MyProject.xml
file in your project root for all target frameworks and all build configurations. Since the dotnet build server builds multiple target framework outputs in parallel there will be a race condition for access to that file.
The simpler configuration, supported in all multi-targeting capable SDK versions, is:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
This will automatically name your XML file with the same base as the assembly name and will create it in the correct obj
folder alongside the assembly.
Warnings can be selectively disabled with the MSBuild standard NoWarn
property. For example:
<PropertyGroup>
<NoWarn>$(NoWarn);IDT002</NoWarn>
</PropertyGroup>
Code | Description |
---|---|
IDT001 | Indicates a referenced XML documentation file could not be loaded or parsed or that the file did not contain documentation in the standard schema. |
IDT002 | Indicates incomplete XML docs for the target assembly or one of its external references. i.e. an inheritance candidate was identified but had no documentaion to inherit. |
IDT003 | May indicate you used <inheritdoc /> on a type/member with no identifiable base. You may correct this warning by using the cref attribute to identify the base explicitly. |
IDT004 | May indicate an incorrect XPath value in a path attribute or a duplicate/superfluous or self-referencing <inheritdoc /> tag. |
When it runs, InheritDocTask
will log a success message to the build output for each processed file, telling you what it did. For example:
InheritDocTask replaced 55 of 55 inheritdoc tags and removed 60 non-public member docs in /path/to/MyProject.xml
If you don't see the message(s), it didn't run for some reason. Check the detailed output from MSBuild (e.g. dotnet build -v detailed
) or use the MSBuild Log Viewer and look for InheritDoc
in the logs for clues. Issue reports are, of course, welcome with good repro steps.