How to Distribute Roslyn Analyzers via NuGet

How to Package Roslyn Analyzers and Code Fixes into a NuGet Package.

Towards the end of 2023 I had some rare downtime and decided to use it to develop a new skill I’ve wanted to learn: leveraging .NET’s impressive Roslyn Compiler Platform to help Akka.NET users be more productive at compile-time.

Roslyn Platform Logo

That interest propelled me to release Akka.Analyzers, a new set of Roslyn Analyzers + Code Fix Providers that we’ve incorporated directly into the core Akka NuGet package as a transitive dependency going forward, so all Akka.NET users and all packages built on top of Akka.NET will benefit directly from our efforts there in perpetuity.

In this blog post I want to discuss one tricky and important obstacle that was not explained very well in any of the literature I found online: how to actually format the Roslyn-enabled NuGet package correctly. This is non-obvious.

About Akka.Analyzers

If you’re curious about what we actually shipped with Akka.Analyzers, you should see our “Akka.NET Jan ‘24 Community Standup - Roslyn Analyzers for Akka.NET” video embedded below.

Distributing Roslyn Packages via NuGet

I’ve shipped 200+ distinct NuGet packages over the past 10 years, and this includes everything from packaging entire executables via NuGet to distributing dotnet tool distributions such as Petabridge.Cmd and Incementalist. I originally assumed that you could ship a Roslyn Analysis package just like any other package, but this is not the case.

The “Build your first analyzer and code fix” tutorial from Microsoft indicates that there’s a distinct packaging project for distributing Roslyn via NuGet, but I couldn’t even find the template they’re referring to - so I looked for inspiration at xUnit’s amazing xunit.analyzer project.

Lo and behold, the .nuspec file, which I haven’t needed to use since .NET Core tooling integrated NuGet package publication directly into the dotnet CLI, has made its triumphant return here:

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2011/10/nuspec.xsd">
	<metadata minClientVersion="2.12">
		<id>xunit.analyzers</id>
		<version>$PackageVersion$</version>
		<title>xUnit.net [Code Analyzers]</title>
		<authors>jnewkirk,bradwilson,marcind</authors>
		<requireLicenseAcceptance>false</requireLicenseAcceptance>
		<license type="expression">Apache-2.0</license>
		<licenseUrl>https://licenses.nuget.org/Apache-2.0</licenseUrl>
		<icon>_content/logo-128-transparent.png</icon>
		<description>xUnit.net is a developer testing framework, built to support Test Driven Development, with a design goal of extreme simplicity and alignment with framework features.

Installing this package provides code analyzers to help developers find and fix frequent issues when writing tests and xUnit.net extensibility code.</description>
		<copyright>Copyright (C) .NET Foundation</copyright>
		<repository type="git" url="https://github.com/xunit/xunit.analyzers" commit="$GitCommitId$" />
		<tags>xunit.analyzers, analyzers, roslyn, xunit, xunit.net</tags>
		<readme>_content/README.md</readme>
		<releaseNotes>https://xunit.net/releases/analyzers/$PackageVersion$</releaseNotes>
	</metadata>
	<files>
		<file target="_content\" src="..\..\README.md" />
		<file target="_content\" src="..\..\tools\media\logo-128-transparent.png" />

		<file target="analyzers\dotnet\roslyn3.11\cs\" src="..\xunit.analyzers.fixes.roslyn311\bin\$Configuration$\netstandard2.0\xunit.analyzers.dll" />
		<file target="analyzers\dotnet\roslyn3.11\cs\" src="..\xunit.analyzers.fixes.roslyn311\bin\$Configuration$\netstandard2.0\xunit.analyzers.fixes.dll" />

		<file target="analyzers\dotnet\roslyn4.2\cs\" src="..\xunit.analyzers.fixes.roslyn42\bin\$Configuration$\netstandard2.0\xunit.analyzers.dll" />
		<file target="analyzers\dotnet\roslyn4.2\cs\" src="..\xunit.analyzers.fixes.roslyn42\bin\$Configuration$\netstandard2.0\xunit.analyzers.fixes.dll" />

		<file target="analyzers\dotnet\roslyn4.4\cs\" src="..\xunit.analyzers.fixes.roslyn44\bin\$Configuration$\netstandard2.0\xunit.analyzers.dll" />
		<file target="analyzers\dotnet\roslyn4.4\cs\" src="..\xunit.analyzers.fixes.roslyn44\bin\$Configuration$\netstandard2.0\xunit.analyzers.fixes.dll" />

		<file target="analyzers\dotnet\roslyn4.6\cs\" src="..\xunit.analyzers.fixes.roslyn46\bin\$Configuration$\netstandard2.0\xunit.analyzers.dll" />
		<file target="analyzers\dotnet\roslyn4.6\cs\" src="..\xunit.analyzers.fixes.roslyn46\bin\$Configuration$\netstandard2.0\xunit.analyzers.fixes.dll" />

		<file target="analyzers\dotnet\roslyn4.8\cs\" src="..\xunit.analyzers.fixes\bin\$Configuration$\netstandard2.0\xunit.analyzers.dll" />
		<file target="analyzers\dotnet\roslyn4.8\cs\" src="..\xunit.analyzers.fixes\bin\$Configuration$\netstandard2.0\xunit.analyzers.fixes.dll" />

		<file target="tools\" src="..\xunit.analyzers.fixes\tools\*.ps1" />
	</files>
</package>

xunit.analyzers is a much more widely used and critical analysis package that Akka.Analyzers is, so it has to handle a fairly large amount of backwards-compatibility complexity. Therefore, I’m going to explain what this .nuspec does by now showing you how Akka.Analyzer’s much smaller solution works.

Akka.Analyzers solution structure inside the src folder

We have the above solution structure:

This setup should serve us well unless we also need to start handling the similar types of backwards-compatibility matrices that xUnit has to support.

The Akka.Analyzers.NuGet project structure itself is relatively simple:

Akka.Analyzers.NuGet project structure

Just the .csproj and the .nuspec file.

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
        <IncludeBuildOutput>false</IncludeBuildOutput>
        <NuspecFile>Akka.Analyzers.nuspec</NuspecFile>
        <NuspecProperties>
            Configuration=$(Configuration);
            PackageVersion=$(VersionPrefix);
            ReleaseNotes=$(PackageReleaseNotes);
            Description=$(Description);
        </NuspecProperties>
    </PropertyGroup>

    <ItemGroup>
      <ProjectReference Include="..\Akka.Analyzers.Fixes\Akka.Analyzers.Fixes.csproj"/>
      <ProjectReference Include="..\Akka.Analyzers\Akka.Analyzers.csproj"/>
    </ItemGroup>
</Project>

Crucially, the .csproj references the two other projects containing the content we want to distribute. It also uses the <NuSpecProperties> property to pass MSBUILD variables (some of them computed from Directory.Build.props, which sits at the root of the solution) directly into the .nuspec file:

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2011/10/nuspec.xsd">
    <metadata minClientVersion="2.12">
        <id>Akka.Analyzers</id>
        <version>$PackageVersion$</version>
        <title>Akka.NET [Code Analyzers]</title>
        <authors>Akka</authors>
        <requireLicenseAcceptance>false</requireLicenseAcceptance>
        <license type="expression">Apache-2.0</license>
        <licenseUrl>https://licenses.nuget.org/Apache-2.0</licenseUrl>
        <icon>logo.png</icon>
        <description>$Description$</description>
        <copyright>Copyright © 2013-2023 Akka.NET Project</copyright>
        <repository type="git" url="https://github.com/akkadotnet/akka.analyzers"/>
        <tags>akka.net, akka.analyzers, akakdotnet, roslyn, analyzers</tags>
        <readme>README.md</readme>
        <releaseNotes>$ReleaseNotes$</releaseNotes>
    </metadata>
    <files>
        <file target="\" src="..\..\README.md" />
        <file target="\" src="..\..\logo.png" />

        <file target="analyzers\dotnet\cs\" src="..\Akka.Analyzers\bin\$Configuration$\netstandard2.0\Akka.Analyzers.dll" />
        <file target="analyzers\dotnet\cs\" src="..\Akka.Analyzers.Fixes\bin\$Configuration$\netstandard2.0\Akka.Analyzers.Fixes.dll" />
    </files>
</package>

Shipping Roslyn Packages Without a .nuspec

After this blog post was shared on Twitter I received a reply with an example demonstrating how to package Roslyn Analyzers using just the .csproj, no .nuspec needed:

And here’s his example from the Faithlife.Analyzers.csproj referenced in the Tweet:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <Description>C# code analyzers used at Faithlife.</Description>
    <PackageTags>roslyn;analyzer</PackageTags>
    <IsPackable>true</IsPackable>
    <IncludeBuildOutput>false</IncludeBuildOutput>
    <NoWarn>$(NoWarn);NU5128;RS2008;CS8600;CS8602;CS8604;CS8619;CS8631</NoWarn>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.5.0" PrivateAssets="all" />
    <PackageReference Update="NETStandard.Library" PrivateAssets="all" />
  </ItemGroup>

  <ItemGroup>
    <None Update="tools\*.ps1" CopyToOutputDirectory="Always" Pack="true" PackagePath="" />
    <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
  </ItemGroup>

</Project>

The final ItemGroup has all of the key lines needed to perform the same output copying we were performing earlier with the .nuspec file. I previously did not know how to do this!

Key Ingredients for Roslyn NuGet Packages

The key that makes all of this work is the destination folder where the analyzers will be copied to inside the NuGet package: analyzers\dotnet\cs\ - this is the default folder that will be scanned by the compiler for additional analyzers and code fix providers when the project is using C# as its programming language. If we were to use F# or VB.NET the destination folder would be different (vb or fs respectively.)

What the xunit.analyzers project is doing differently is accounting for different versions of the Roslyn compiler platform in their NuGet output:

<file target="analyzers\dotnet\roslyn3.11\cs\" src="..\xunit.analyzers.fixes.roslyn311\...\xunit.analyzers.dll" />

That’s a level of complexity you are unlikely to need, but that’s why their .nuspec looks the way it does.

I hope you found this helpful - please feel free to check out the Akka.Analyzers source code if you have any questions.

Discussion, links, and tweets

I'm the CTO and founder of Petabridge, where I'm making distributed programming for .NET developers easy by working on Akka.NET, Phobos, and more..