Incremental Roslyn Source Generators: Using 3rd-Party Libraries – Part 6

We previously talked about how to change the source code generation based on current project dependencies. In this article, the Source Generator itself needs a 3rd-party library, in our case Newtonsoft.Json. This library is a development dependency and will not be rolled out to production.

In diesem Artikel:

pg
Pawel Gerr ist Architekt und Consultant bei Thinktecture. Er hat sich auf .NET Core Backends spezialisiert und kennt Entity Framework von vorne bis hinten.

More information about the Smart Enums and the source code can be found on GitHub:

I recommend to read part 5 of the series because this article is based on the results of the previous one.

In this article, the Source Generator will generate C# code based on the content of JSON, deserialized using the library Newtonsoft.Json. I choose Newtonsoft.Json over System.Text.Json because the latter comes with the framework and is available by default, i.e., it doesn’t suit well as an example.

Expected Results

The DemoCodeGenerator, implemented in the previous part of this series, will generate not just the property Items but a few attributes as well.

				
					namespace DemoConsoleApplication
{
   [Translation("en", "Product category")]
   [Translation("de", "Produktkategorie")]
   partial class ProductCategory
   {
      ...
				
			

JSON will be the source for the translations.

				
					{
  "ProductCategory": {
    "en": "Product category",
    "de": "Produktkategorie"
  }
}

				
			

The TranslationAttribute, as with the EnumGenerationAttribute, is implemented in the project DemoLibrary because both of them are going to be published along with the production code. (See part 1 for more information)

				
					namespace DemoLibrary;

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class TranslationAttribute : Attribute
{
   public string Language { get; }
   public string Translation { get; }

   public TranslationAttribute(string language, string translation)
   {
      Language = language;
      Translation = translation;
   }
}

				
			

Refactoring

For readers coming from Part 5 of this series, a few preparations are necessary to be able to tell apart the important changes from the refactorings.

The method Generate of the interface ICodeGenerator gets an additional argument translations.

				
					public interface ICodeGenerator : IEquatable<ICodeGenerator> 
{ 
      string Generate(DemoEnumInfo enumInfo, IReadOnlyDictionary<string, string> translations);
      ...
				
			

The DemoCodeGenerator is using a StringBuilder for generation of the C# code, so it is easier to render additional code in between. The code is split before partial class {name} for future generation of the TranslationAttribute.

				
					   public string Generate(DemoEnumInfo enumInfo, IReadOnlyDictionary<string, string> translations) 
   { 
      var ns = enumInfo.Namespace; 
      var name = enumInfo.Name; 
 
      var sb = new StringBuilder(@$"// <auto-generated /> 
#nullable enable 
 
// generation counter: {Interlocked.Increment(ref _counter)} 
 
using System.Collections.Generic; 
using DemoLibrary; 
 
{(ns is null ? null : $@"namespace {ns} 
{{")}"); 
 
      // TODO: TranslationAttribute

      sb.Append(@$" 
   partial class {name} 
   {{ 
      private static IReadOnlyList<{name}>? _items; 
      public static IReadOnlyList<{name}> Items => _items ??= GetItems(); 
 
      private static IReadOnlyList<{name}> GetItems() 
      {{ 
         return new[] {{ {String.Join(", ", enumInfo.ItemNames)} }}; 
      }} 
   }} 
{(ns is null ? null : @"} 
")}"); 
 
      return sb.ToString(); 
   } 
				
			

Adding 3rd-Party Library

A 3rd-party library installed via NuGet requires a few adjustments, so the Source Generator finds the required DLLs when (a) the Source Generator itself is installed via NuGet and (b) the Source Generator is referenced directly via project reference.

After installation of the NuGet package to DemoSourceGenerator.csproj, the package reference needs the attribute PrivateAssets set to all, so it stays a development dependency and is not rolled out to production. Additionally, the attribute GeneratePathProperty must be set to true to get a path to the assets of the NuGet package. The path is built from the package name by replacing the . with _ and by prefixing it with Pkg

				
					<Project Sdk="Microsoft.NET.Sdk">
   ...
   <ItemGroup>
      ...
      
      <PackageReference Include="Newtonsoft.Json"
                        Version="13.0.1"
                        PrivateAssets="all"
                        GeneratePathProperty="true" /> 
 
      <None Include="$(PkgNewtonsoft_Json)\lib\netstandard2.0\*.dll"
            Pack="true"
            PackagePath="analyzers/dotnet/cs"
            Visible="false" /> 
   </ItemGroup>
   
</Project>

				
			

As with the DLL of the Source Generator, all dependencies must be copied to analyzers/dotnet/cs when building a NuGet package.

If the Source Generator is not referenced directly by another project, then no further adjustments of the DemoSourceGenerator.csproj are necessary. Otherwise, the compiler needs a hint of where to search for further dependencies. Each dependency must be specified using the TargetPathWithTargetPlatformMoniker.

				
					<Project Sdk="Microsoft.NET.Sdk"> 
   ...
 
    
   <PropertyGroup> 
      <GetTargetPathDependsOn>
          $(GetTargetPathDependsOn);GetDependencyTargetPaths
      </GetTargetPathDependsOn> 
   </PropertyGroup> 
 
   <Target Name="GetDependencyTargetPaths"> 
      <ItemGroup> 
         <TargetPathWithTargetPlatformMoniker
            Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll"
            IncludeRuntimeDependency="false" /> 
      </ItemGroup> 
   </Target> 
 
</Project> 

				
			

Parsing JSON

The last step is to parse JSON in DemoSourceGenerator and pass the translations to the code generators.

There are 2 flaws in this demo: JSON is hard-coded, and there is no try-catch when parsing JSON. Both issues are going to be addressed in the next part of this series.

				
					[Generator] 
public class DemoSourceGenerator : IIncrementalGenerator 
{ 
   private static readonly IReadOnlyDictionary<string, string> _noTranslations
        = new Dictionary<string, string>(); 
 
   private const string _TRANSLATIONS = @" 
{ 
   ""ProductCategory"": { 
      ""en"":  ""Product category"", 
      ""de"": ""Produktkategorie"" 
   } 
}"; 
 
   ...
 
   private static void GenerateCode( 
      SourceProductionContext context, 
      (DemoEnumInfo, ImmutableArray<ICodeGenerator>) tuple) 
   { 
      var (enumInfo, generators) = tuple; 
 
      if (generators.IsDefaultOrEmpty) 
         return; 
 
      var translationsByClassName = JsonConvert.DeserializeObject<
            Dictionary<string, IReadOnlyDictionary<string, string>>>(_TRANSLATIONS); 
 
      foreach (var generator in generators.Distinct()) 
      { 
         if (translationsByClassName is null
            || !translationsByClassName.TryGetValue(enumInfo.Name, out var translations)) 
        {
            translations = _noTranslations; 
        }

         var ns = enumInfo.Namespace is null ? null : $"{enumInfo.Namespace}."; 
         var code = generator.Generate(enumInfo, translations); 
 
         if (!String.IsNullOrWhiteSpace(code)) 
            context.AddSource($"{ns}{enumInfo.Name}{generator.FileHintSuffix}.g.cs", code); 
      } 
   } 
} 

				
			

The DemoCodeGenerator generates attributes according to the provided translations.

				
					public sealed class DemoCodeGenerator : ICodeGenerator 
{ 
   ...
   
   public string Generate(DemoEnumInfo enumInfo, IReadOnlyDictionary<string, string> translations) 
   { 
      ...
 
{(ns is null ? null : $@"namespace {ns} 
{{")}"); 
 
      GenerateTranslationAttributes(sb, translations); 
 
      sb.Append(@$" 
   partial class {name} 
      ...
   } 
 
   private static void GenerateTranslationAttributes( 
      StringBuilder sb, 
      IReadOnlyDictionary<string, string> translations) 
   { 
      foreach (var kvp in translations) 
      { 
         sb.Append(@" 
   [Translation(""").Append(kvp.Key).Append("\", \"").Append(kvp.Value).Append("\")]"); 
      } 
   } 
 
				
			

Summary

Referencing and using other NuGet packages inside a Source Generator works almost the same as with any other project. But your Source Generator might be not the only one using a 3rd-party dependency. A project referencing multiple Source Generators with incompatible dependencies will run into issues.

Kostenloser
Newsletter

Aktuelle Artikel, Screencasts, Webinare und Interviews unserer Experten für Sie

Verpassen Sie keine Inhalte zu Angular, .NET Core, Blazor, Azure und Kubernetes und melden Sie sich zu unserem kostenlosen monatlichen Dev-Newsletter an.

Newsletter Anmeldung
Diese Artikel könnten Sie interessieren
.NET
pg

Smart Enums in .NET: Integration with Frameworks and Libraries

Learn how to seamlessly integrate Smart Enums with essential .NET frameworks and libraries. This article covers practical solutions for JSON serialization, ASP.NET Core model binding for both Minimal APIs and MVC controllers, and Entity Framework Core persistence using value converters. Discover how Thinktecture.Runtime.Extensions provides dedicated packages to eliminate integration friction and maintain type safety across your application stack.
21.09.2025
.NET
pg

Value Objects in .NET: Enhancing Business Semantics

Value objects are fundamental building blocks in Domain-Driven Design, serving far more than simple data wrappers. This article explores their strategic importance in bridging technical code and business concepts, enforcing domain rules, and fostering clearer communication with domain experts. Learn how to build robust aggregates, cultivate ubiquitous language, and encapsulate domain-specific behavior using Thinktecture.Runtime.Extensions in .NET applications.
16.09.2025
.NET
pg

Pattern Matching with Discriminated Unions in .NET

Traditional C# pattern matching with switch statements and if/else chains is error-prone and doesn't guarantee exhaustive handling of all cases. When you add new types or states, it's easy to miss updating conditional logic, leading to runtime bugs. The library Thinktecture.Runtime.Extensions solves this with built-in Switch and Map methods for discriminated unions that enforce compile-time exhaustiveness checking.
26.08.2025
.NET
pg

Value Objects in .NET: Integration with Frameworks and Libraries

Value Objects in .NET provide a structured way to improve consistency and maintainability in domain modeling. This article examines their integration with popular frameworks and libraries, highlighting best practices for seamless implementation. From working with Entity Framework to leveraging their advantages in ASP.NET, we explore how Value Objects can be effectively incorporated into various architectures. By understanding their role in framework integration, developers can optimize data handling and enhance code clarity without unnecessary complexity.
12.08.2025
.NET
pg

Smart Enums: Adding Domain Logic to Enumerations in .NET

This article builds upon the introduction of Smart Enums by exploring their powerful capability to encapsulate behavior, a significant limitation of traditional C# enums. We delve into how Thinktecture.Runtime.Extensions enables embedding domain-specific logic directly within Smart Enum definitions. This co-location of data and behavior promotes more cohesive, object-oriented, and maintainable code, moving beyond scattered switch statements and extension methods. Discover techniques to make your enumerations truly "smart" by integrating behavior directly where it belongs.
29.07.2025
.NET
pg

Discriminated Unions: Representation of Alternative Types in .NET

Representing values that may take on multiple distinct types or states is a common challenge in C#. Traditional approaches—like tuples, generics, or exceptions—often lead to clumsy and error-prone code. Discriminated unions address these issues by enabling clear, type-safe modeling of “one-of” alternatives. This article examines pitfalls of conventional patterns and introduces discriminated unions with the Thinktecture.Runtime.Extensions library, demonstrating how they enhance code safety, prevent invalid states, and improve maintainability—unlocking powerful domain modeling in .NET with minimal boilerplate.
15.07.2025