Article series
- Code sharing of the future
- Better experience through Roslyn Analyzers and Code Fixes
- Testing Source Generators, Roslyn Analyzers and Code Fixes
- Increasing Performance through Harnessing of the Memoization
- Adapt Code Generation Based on Project Dependencies
- Using 3rd-Party Libraries ⬅
- Using Additional Files
- High-Level API – ForAttributeWithMetadataName
- Reduction of resource consumption in IDE
- Configuration
- Logging
More information about the Smart Enums and the source code can be found on GitHub:
- Smart Enums
- Source Code (see commits starting with message “Part 6”)
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
{
string Generate(DemoEnumInfo enumInfo, IReadOnlyDictionary 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 translations)
{
var ns = enumInfo.Namespace;
var name = enumInfo.Name;
var sb = new StringBuilder(@$"//
#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
.
...
...
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
.
...
$(GetTargetPathDependsOn);GetDependencyTargetPaths
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 _noTranslations
= new Dictionary();
private const string _TRANSLATIONS = @"
{
""ProductCategory"": {
""en"": ""Product category"",
""de"": ""Produktkategorie""
}
}";
...
private static void GenerateCode(
SourceProductionContext context,
(DemoEnumInfo, ImmutableArray) tuple)
{
var (enumInfo, generators) = tuple;
if (generators.IsDefaultOrEmpty)
return;
var translationsByClassName = JsonConvert.DeserializeObject<
Dictionary>>(_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 translations)
{
...
{(ns is null ? null : $@"namespace {ns}
{{")}");
GenerateTranslationAttributes(sb, translations);
sb.Append(@$"
partial class {name}
...
}
private static void GenerateTranslationAttributes(
StringBuilder sb,
IReadOnlyDictionary 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.