NuGet Packaging: Packages with Source Generators and Scriban

How to package Roslyn source generator libraries that are using Scriban for code generation

Dec 6, 2023

Engineering

Nuget Packaging: Packages with Source Generators and Scriban
Nuget Packaging: Packages with Source Generators and Scriban

NuGet Packaging Series

This article is part of the Creating NuGet Packages series. You can find the complete list of articles in the series at the end.


Introduction

In the world of .NET development, Roslyn source generators are an invaluable tool for code generation, aiding in everything from performance optimization to reducing boilerplate code. However, the process of packaging these source generators as NuGet packages can be tricky.

This article will walk you through a comprehensive guide on how to package a Roslyn source generator with Scriban for code templating. Moreover, it covers specific issues you might encounter when using the default Rider source generator project template and how to resolve them.

The Example Project

I created a compact and straightforward Roslyn incremental source generator for this exercise. It enhances all (partial) classes marked with the HelloThere attribute by adding an extra method that prints the message "Hello there from {{class name}}."

To test it, make a partial class and annotate it with theHelloThere attribute and the source generator will automatically generate an attribute class and partial class containing the HelloThere method. It should look like this:

[HelloThere]
public partial class HelloThereTestA
{
}

Packing as Source Generator

The first step is easy. To ensure that the library is packed as a source generator, add this line to the .csproj file:

<!-- This ensures the library will be packaged as a source generator when we use `dotnet pack` -->
<ItemGroup>
    <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>


Scriban

I prefer using Scriban as the templating engine to generate code instead of relying on StringBuilder or raw string literals. Scriban is a highly efficient and secure text-templating engine. It offers exceptional speed and power, comparable to Razor and Liquid, but with optimized performance.

It's a pretty popular templating engine for .NET, but there are some nuances when it comes to using it with source generators, which we will discuss here.

Why Use Scriban in Source Generators?

In the context of source generators, Scriban can be employed for the same reason you'd use any templating engine —to generate code dynamically based on templates. However, Scriban has a few advantages:

  1. Performance: It's incredibly fast and crucial when generating code at compile time.

  2. Flexibility: Offers a variety of features like custom functions and filters.

  3. Safety: Scriban performs automatic HTML escaping, which can sometimes be useful.


Using Scriban with Source Generators

To use Scriban with source generators and be able to package the library, there are a few steps needed for it to work as expected and produce a NuGet package out of your generator.

Let's dissect all Scriban-related parts:

Including the Scriban Sources

For the source generator to work with Scriban, the Scriban sources must be included with it. Starting from Scriban 3.2.1+, this can be enabled using the <PackageScribanIncludeSource>true</PackageScribanIncludeSource> property:

<!-- 
👇 SCRIBAN RELATED
Starting with Scriban 3.2.1+, the package includes a source so you can internalize your 
usage of Scriban into your project. This can be useful in an environment where you can't easily consume 
NuGet references (e.g., Roslyn Source Generators).
see: https://github.com/scriban/scriban#source-embedding
-->
<PackageScribanIncludeSource>true</PackageScribanIncludeSource>

Needed Packages

Since the Source Generators must target netstandard2.0, as per the manual, we must include these packages:

<!-- SCRIBAN RELATED -->
<ItemGroup>
    <PackageReference Include="Scriban" Version="5.9.0" IncludeAssets="Build" />
    <PackageReference Include="Microsoft.CSharp" Version="4.7.0"/>
    <PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4"/>
</ItemGroup>

After this, Scriban is ready to be packaged with your source generator. I won't explain how to set up Scriban for templating in this article since the focus is on packaging source generators with it. I hope to cover that in future articles.

On to the next thing to solve...


Rider's Source Generator Template

JetBrains Rider provides a nice project template for source generators; it works out of the box (compiling, debugging, and testing) but has some issues when packaging the library.

To be precise, the problem lies with this part:

<ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0-beta1.23364.2">
        <PrivateAssets>all</PrivateAssets>
        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.7.0" />
</ItemGroup>

If you try to package the source generator using these references, it will fail with a warning:

warning RS1038: This compiler extension should not be implemented in an assembly containing a reference to Microsoft.CodeAnalysis.Workspaces. The Microsoft.CodeAnalysis.Workspaces assembly is not provided during command line compilation scenarios, so references to it could cause the compiler extension to behave unpredictably.

This is because the Microsoft.CodeAnalysis.Workspaces assembly is not provided during command-line compilation scenarios, which could lead to unpredictable behavior if your source generator attempts to utilize types or members from this assembly.

To overcome this issue, delete or comment out those references and set new ones like this:

<!--
 Use these references for the generator instead of Rider's defaults.
 Rider's defaults produce errors when packaging as Nuget. 
 -->
<ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0-2.final" PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0-beta1.23420.2" PrivateAssets="all" />
</ItemGroup>

The Final .csproj File

After this, you should be able to package your source generator library as usual. Here's the complete .csproj file for a review:

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

    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>        
        <Nullable>enable</Nullable>
        <LangVersion>latest</LangVersion>

        <!-- SOURCE GENERATOR IMPORTANT SETTINGS -->
        <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
        <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>        
        <IsRoslynComponent>true</IsRoslynComponent>

        <RootNamespace>Packaging.Generators</RootNamespace>

        <!-- NUGET PACKAGE SETTINGS -->
        <IsPackable>true</IsPackable>
        <PackageId>Packaging.Generators</PackageId>
        <Title>Example Library for NuGet Packaging</Title>
        <Version>1.0.0</Version>
        <Authors>Dinko Pavicic</Authors>
        <Description>Example NuGet package for NuGet packaging with source generator and Scriban.</Description>
        <Copyright>Copyright (c) Dinko Pavicic 2024</Copyright>
        <PackageProjectUrl>https://dinkopavicic.com</PackageProjectUrl>
        <RepositoryUrl>https://github.com/dpavicic/Research-Nuget-Packaging/tree/master/Packaging-Generators</RepositoryUrl>
        <RepositoryType>git</RepositoryType>
        <PackageReadmeFile>README.md</PackageReadmeFile>
        <PackageLicenseExpression>MIT</PackageLicenseExpression>
        <PackageTags>nuget, packaging, generator, example</PackageTags>
        <PackageReleaseNotes>Version 1.0.0: First version of example library for NuGet packaging with source generator and Scriban.</PackageReleaseNotes>
        <PackageIcon>package_icon.png</PackageIcon>

        <!-- 
        👇 SCRIBAN RELATED
        Starting with Scriban 3.2.1+, the package includes a source so you can internalize your 
        usage of Scriban into your project. This can be useful in an environment where you can't easily consume 
        NuGet references (e.g., Roslyn Source Generators).
        see: https://github.com/scriban/scriban#source-embedding
        -->
        <PackageScribanIncludeSource>true</PackageScribanIncludeSource>
    </PropertyGroup>

    <!-- Include package assets -->
    <ItemGroup>
        <None Include="README.md" Pack="true" PackagePath="\"/>
        <None Include="images\package_icon.png" Pack="true" PackagePath="\"/>
    </ItemGroup>

    <!-- This ensures the library will be packaged as a source generator when we use `dotnet pack` -->
    <ItemGroup>
        <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
    </ItemGroup>

    <!--
     Use these references for the generator instead of Rider's defaults.
     Rider's defaults produce errors when packaging as Nuget. 
     -->
    <ItemGroup>
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0-2.final" PrivateAssets="all" />
        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0-beta1.23420.2" PrivateAssets="all" />
    </ItemGroup>
    
    <!-- 
        This (below) is Rider defaults. Unfortunately, this produces errors when packaging as Nuget.
        It seems that the problem lies with Microsoft.CodeAnalysis.CSharp.Workspaces
        
        warning RS1038: This compiler extension should not be implemented in an assembly containing a     reference to Microsoft.CodeAnalysis.Workspaces. The Microsoft.CodeAnalysis.Workspaces assembly is not provided during command-line compilation scenarios, so references to it could cause the compiler extension to behave unpredictably.
    -->
    <!--
    <ItemGroup>
        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0-beta1.23364.2">
            <PrivateAssets>all</PrivateAssets>
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
        </PackageReference>
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.7.0" />
    </ItemGroup>
    -->

    <!-- SCRIBAN RELATED -->
    <ItemGroup>
        <PackageReference Include="Scriban" Version="5.9.0" IncludeAssets="Build" />
        <PackageReference Include="Microsoft.CSharp" Version="4.7.0"/>
        <PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4"/>
    </ItemGroup>
    
    <!-- Include Scriban code templates -->
    <ItemGroup>      
      <EmbeddedResource Include="Templates\HelloThereClass.tpl" />
      <EmbeddedResource Include="Templates\HelloThereAttr.tpl" />
    </ItemGroup>

</Project>


What Have We Learned?

I hope this article provided information on packaging source generator projects and other related topics we discussed.

In this article, we learned:

  • How to package source generator projects

  • How to package if using Scriban for templating

  • How to fix Rider's package reference to be able to pack

Example Project on GitHub

An example project for this article can be found here:

Workshop-Nuget-Packaging

Creating NuGet Packages Series

This article is part of the Creating NuGet Packages series. If you enjoyed this one and want more, here is the complete list in the series:

Happy Coding!

More Articles

Thanks
for Visiting

.. and now that you've scrolled down here, maybe I can invite you to explore other sections of this site

Thanks
for Visiting

.. and now that you've scrolled down here, maybe I can invite you to explore other sections of this site