Alex KlausDeveloper  |  Architect  |  Leader
5 steps to better NuGet package
19 September 2020

5 steps to better NuGet package

Five simple but often overlooked steps to provide a better experience to the devs using your NuGet package.

Step 1. PDBs and the Source Code

Improve the debugging experience and the exception reports.

The PDB files are not necessary for the app to run, but they make the exception reports more informative by providing the file and line numbers in the Exception.StackTrace. Nowadays, supplying the PDBs is a well expected a courtesy from NuGet developers.

1.1. PDBs for Symbol Server

In the old days, prior availability of NuGet.org Symbol Server devs used to include the PDB in the main NuGet package. But these days:

Including PDBs in the .nupkg is generally no longer recommended as it increases the size of the package and thus restore time for projects that consume your package, regardless of whether the user needs to debug through the source code of your library or not.

The modern approach is distributing a symbol package (.snupkg) that contain the PDBs along with the main package. It’s supported by most vendors with Azure DevOps being the biggest outsider (a feature request is still “under review”).

Here is the official guidance for .NET projects. Simply add these two lines to the csproj file:

<PropertyGroup>
  <IncludeSymbols>true</IncludeSymbols>
  <SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>

On the consumer side, make sure that your IDE is configured for the NuGet.org (or Azure, etc.) Symbol Server to allow stepping into package code when debugging.

1.2. Source Link. Taking it one step further

The PDBs are cool, but due to MSIL optimisations they may differ from the actual code. Fortunately for devs, there is a way of ”Stepping Into” the actual source of your NuGet while debugging. It’s called Source Link from the .NET Foundation – ”a language- and source-control agnostic system for providing source debugging experiences for binaries“.

It’s supported by Visual Studio 15.3+, can download and display the appropriate commit-specific source, enabling breakpoints and all other sources debugging experience. Supports private and public GitHub, Azure DevOps and many others.

It’s trivial to setup (official docs) – just add a development dependency to the project file (depends on the type of your repo) along with some flags:

<PropertyGroup>
    <PublishRepositoryUrl>true</PublishRepositoryUrl>
    <EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>
<ItemGroup>
  <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>

Check out Scott Hanselman’s experience of using Source Link and post of Aaron Stannard for setting things up on the consumer side.

Step 2. XML documentation

Make the documentation available via IntelliSense in the projects consuming the NuGet packages.

Of course, we document our code with XML comments in spite of their verbosity and perseverance on using the XML tags and ceremonies. And a particular way enabling the XML docs has entrenched among the Visual Studio devs – right-click on the project and select Properties, select the Build tab, and check XML documentation file.

It does the trick and adds to the csproj file:

<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>

The downsides of this approach:

  • It forces you to think of the path to the file (usually it’s far from being important).
  • Requires extra work for NuGet packages having multiple targets (adding ‘if’ conditions to the csproj, etc).

Little known that there is another way of producing the XML documentation file by simply adding this to the csproj file:

<GenerateDocumentationFile>true</GenerateDocumentationFile>

It will handle multiple targets automatically and will be picked up by the NuGet packaging tool. Send your kudos to this SO post.

Step 3. Sign it.

Sing it for using in strong-named assemblies.

We may think that signing assemblies is a thing of the past. Scratching my head I recall that it was used mainly for 3 reasons:

  1. For publishing to GAC (it’s Global Assembly Cache for those who forgot). Lucky for us, it became clear a long time ago that GAC was more hustle than it was worth.
  2. For managing different versions of the same assembly.
  3. For ensuring the authenticity of an assembly that it hasn’t been tampered with.

Maybe we still wouldn’t care about signing NuGets if point #3 didn’t lead to one constraint – signed assemblies can refer to only other signed ones (docs).

Here the ghost of the past comes back – being strong-name-friendly is still a thing. Follow the guidelines to sign your packages:

  • generate a .snk and
  • add a couple of lines to the csproj
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>My.snk</AssemblyOriginatorKeyFile>

And last, official Microsoft advice to OSS developers:

If you are an open-source developer and you want the identity benefits of a strong-named assembly for better compatibility with .NET Framework, consider checking in the private key associated with an assembly to your source control system.

Step 4. CLS compliance

Make it usable in all the .NET languages.

Often, devs use the terms ”.NET” and ”C#” interchangeably completely forgetting about other .NET languages, such as F#, C++, VB, IronPython, etc. And some might be surprised that compiling your source code into Microsoft Intermediate Language (MSIL) doesn’t make it usable by components written in other languages (more on the topic).

To make the library consumable in any .NET language, it has to be CLS compliant. The Common Language Specification (CLS) defines naming restrictions, data types, and rules to which assemblies must conform if they will be used across programming languages.

The CLS compliance limits use of some C# features, most vividly:

The good thing is that it’s applied only to the externally facing interfaces of your package. Under the hood of your methods, you still have the liberty to write language-specific code.

If you’re OK with some sacrifise then simply add to AssemblyInfo.cs:

[assembly: CLSCompliant(true)]

It will throw a compile error if your code is not CLS compliant.

Step 5. Versioning

Use the Semantic Versioning of the NuGet packages linked to Git commits.

Semantic Versioning (SemVer 2.x) is handy in managing customer expectations and minimising frustration after introducing breaking changes.

As managing versions can get messy, it’s a good idea to link versions to Git commits… Unless your package versioning has complex dependencies that must be controlled outside of the Git repository (e.g. in TeamCity or other CI/CD tool).

And here are 3 popular options:

My preference goes to MinVer as it much simpler and works better for smaller teams with straightforward processes. Check out the package and the author’s comparison of MinVer with its counterparts.

Adding MinVer is as trivial as adding Source Link described above:

<ItemGroup>
  <PackageReference Include="MinVer" Version="2.3.0" PrivateAssets="All" />
</ItemGroup>

Note, that with either solution you’ll likely need to turn on deterministic builds which is off by default for .NET Core projects (more on why deterministic is good) by adding:

<PropertyGroup>
  <Deterministic>true</Deterministic>
</PropertyGroup>

Have a good sample handy

When writing a new NuGet, check on the best practices used in the most popular packages (e.g. Newtonsoft.Json). But often, their .csproj files are overloaded due to a high level of compatibility and flexibility.

As an alternative, you may use DomainResult a neat and tiny NuGet package that I released recently. Hope it helps.

Have your say in the comments below, on Reddit thread, Twitter or LinkedIn.