Copy XML documentation from NuGet package to project build folder!

At my current client, we are building an API that is put together by re-usable “API parts”, eg. ASP.NET Core Application Parts, which works wonders by the way. We can have multiple parts of the API split into small NuGet packages that can be re-used in other systems, that way we only have to implement system specific code, the rest is reused, including documentation etc.

Talking about documentation, brings us directly to the issue. We use SwashBuckle to generate our Swagger definition and Swagger UI, and Swashbuckle requires XML documentation, to be able to include documentation from our Controllers and models. I thought we could probably just add a checkbox “Add XML documentation from NuGet package, on build”, but… Unfortunately not.

After a lot of research, and looking at NuGets github issues stating this problem I came up with a solution that works both at build time and at publish time (which apparently is handled differently).

The Setup

We have the following projects:

RestAPI (Root website, that will contain multiple API packages) ASP.NET Core website Contains Swagger page System specific API Service implementations

BasicAPI (NuGet package) ASP.NET Core website Contains API endpoints, Models, Service interfaces etc. Public XML documentation for its own API and Models

.. more NuGet packages to come

Include XML documentation, when building NuGet package with . csproj file

Our application is very simple, so we have chosen to pack nuget packages with our .csproj file instead of the more explicit .nuspec file.

Including XML documentation into a NuGet package is thankfully quite easy, and it should be, as it is the main way that documentation follows C# code.

The easiest way is to add the following PropertyGroup into your nuget package .csproj file:

<PropertyGroup> <GenerateDocumentationFile>true</GenerateDocumentationFile> <NoWarn>$(NoWarn);1591</NoWarn> </PropertyGroup>

This will do as it states, GenerateDocumentationFile . Generate a Documentation file, when you build the project. This file is also automatically added to your NuGet Package when you pack it.

Copy XML docs from NuGet package to your main projects build output

This is where it gets tricky. I was hoping that I could just add another boolean configuration. When looking through the issues for nuget, msbuild and dotnet on github, it did seem like they were working on something, and had implemented a way to add XML documentation from your PROJECT references/dependencies, but not your NuGet dependencies.

Luckily, we can add our own targets to the MSBuild task list and copy the XML docs ourselfs:

First add CopyToOutputDirectory to your main projects .csproj file, for the PackageReference you want to copy XML doc from. Like so:

<ItemGroup> <PackageReference Include="BasicAPI" Version="0.1.0-develop-6982"> <CopyToOutputDirectory>lib

etcoreapp2.1\*</CopyToOutputDirectory> </PackageReference> <PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.8" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="2.2.0" /> ... etc <PackageReference Include="Swashbuckle.AspNetCore" Version="4.0.1" /> </ItemGroup>

The relative path added inside the CopyToOutputDirectory is the relative path from the root of the NuGet package folder, to where the XML docs are located.

In .NET Core, our NuGet packages are located at:

%USERPROFILE%\.nuget\packages\{PackageName}\{PackageVersion}

And the XML documentation file would be located at:

%USERPROFILE%\.nuget\packages\{PackageName}\{PackageVersion}\lib

etcoreapp2.1\{PackageName}.xml

Setup build targets to copy XML docs from NuGet package after Build

To copy the file after build time, we must add a target to MSBuild that does that. Add the following to your main projects . csproj file:

<Target Name="CopyPackages" AfterTargets="Build"> <ItemGroup> <PackageReferenceFiles Condition="%(PackageReference.CopyToOutputDirectory) != ''" Include="$(NugetPackageRoot)\%(PackageReference.FileName)\%(PackageReference.Version)\%(PackageReference.CopyToOutputDirectory)" /> </ItemGroup> <Copy SourceFiles="@(PackageReferenceFiles)" DestinationFolder="$(OutDir)" /> </Target>

To copy the files from the NuGet package to our build output, we add a Target named “CopyPackages”, this name is for our own use and has no effect. After that, we are stating that we want this Target to run after the Build has finished.

When then create a Variable called PackageReferenceFiles that finds all PackageReference that has a property called CopyToOutputDirectory (the one we added in the last section). It then sets that variables value to be a combination of the location of the NuGetPackageRoot and relative path we set earlier.

Finally it asks MSBuild to do a file copy from the variable PackageReferenceFiles to the Builds Output directory.

Setup build targets to copy XML docs from NuGet package before Publish

For some odd reason, publish is done in multiple steps and it is not just a file copy from the build procedure, at least for web applications. That means that our OutDir will not be correct, and needs to be changed to PublishDir , we also need to make sure that this runs before the Target called PrepareForPublish .

I have tried multiple solutions to make this into one Target, but to finally gave into:

Keep it simple, stupid

And just added another Target with this small change.

So to copy XML docs from NuGet package before Publish, add this to your main projects .csproj file:

<Target Name="CopyPackagess" BeforeTargets="PrepareForPublish"> <ItemGroup> <PackageReferenceFiles Condition="%(PackageReference.CopyToOutputDirectory) != ''" Include="$(NugetPackageRoot)\%(PackageReference.FileName)\%(PackageReference.Version)\%(PackageReference.CopyToOutputDirectory)" /> </ItemGroup> <Copy SourceFiles="@(PackageReferenceFiles)" DestinationFolder="$(PublishDir)" /> </Target>

Adding the NuGet packages XML doc to Swagger (Swashbuckle)

Finally, you just have to add the XML docs to your Swagger definition like so:

public void ConfigureSwagger(IServiceCollection services, object identityServerConfiguration) { // Register the Swagger generator, defining one or more Swagger documents services.AddSwaggerGen(options => { // integrate xml comments options.IncludeXmlComments(XmlCommentsFilePath(GetAssemblyName() + ".xml")); options.IncludeXmlComments(XmlCommentsFilePath("BasicAPI.xml")); // ... } private static string XmlCommentsFilePath(string xmlDocFileName) => Path.Combine(AppContext.BaseDirectory, xmlDocFileName); private static string GetAssemblyName() => typeof(Startup).GetTypeInfo().Assembly.GetName().Name;

I hope this saves you some time, as this took me a small evening to get right.

// André Snede Kock