Automating Code Mappings in .NET with Roslyn Source Generators

Use Case

Many business apps store reference codes in a database and reuse them across services, UI labels, and rules. Example:

Code Description
1010001 Processing
1010002 Completed
1010003 Cancelled

The traditional pattern is to hard-code these values in C#:

public static class OperationCodes
{
    public const string Processing = "1010001";
    public const string Completed  = "1010002";
    public const string Cancelled  = "1010003";
}

Problems: every DB change requires code edits and redeploys, mismatches can slip in, and the process is tedious.

Solution: Roslyn Source Generators

Roslyn Source Generators create C# code during compilation. Instead of hand-writing constants, the compiler emits them automatically from your source of truth (DB, JSON, etc.).

  • Runs at compile-time (no runtime reflection or warm-up).
  • Works in .NET 6/7/8+ with Visual Studio 2022 and the dotnet CLI.
  • Generator project should target netstandard2.0 for analyzer compatibility.

Project Layout

CodeGenDemo.sln
├─ CodeGenDemo/                 ← Console app (uses generated code)
│  └─ CodeGenDemo.csproj
└─ CodeGenDemo.Generators/      ← Roslyn source generator (analyzer)
   └─ CodeGenDemo.Generators.csproj

1) Generator Project (CodeGenDemo.Generators)

CodeGenDemo.Generators.csproj — target netstandard2.0 and reference Roslyn packages:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>latest</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
  </ItemGroup>
</Project>

OperationCodeGenerator.cs — emits the OperationCodes class:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;

namespace CodeGenDemo.Generators
{
    [Generator]
    public class OperationCodeGenerator : ISourceGenerator
    {
        public void Initialize(GeneratorInitializationContext context) { }

        public void Execute(GeneratorExecutionContext context)
        {
            // In a real app, load from SQL/JSON/config.
            var codes = new (string Code, string Name)[]
            {
                ("1010001", "Processing"),
                ("1010002", "Completed"),
                ("1010003", "Cancelled")
            };

            var sb = new StringBuilder();
            sb.AppendLine("namespace CodeGenDemo");
            sb.AppendLine("{");
            sb.AppendLine("    public static class OperationCodes");
            sb.AppendLine("    {");

            foreach (var c in codes)
            {
                var safeName = c.Name.Replace(" ", "");
                sb.AppendLine($"        public const string {safeName} = \"{c.Code}\";");
            }

            sb.AppendLine("    }");
            sb.AppendLine("}");

            context.AddSource("OperationCodes.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8));
        }
    }
}

Why netstandard2.0? Analyzers are loaded by the compiler’s host. Targeting net8.0 often causes load failures like: Could not load file or assembly 'System.Runtime, Version=8.0.0.0'. netstandard2.0 avoids this.

2) Console App Project (CodeGenDemo)

CodeGenDemo.csproj — reference the generator as an Analyzer (not a normal library):

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\\CodeGenDemo.Generators\\CodeGenDemo.Generators.csproj"
                      OutputItemType="Analyzer"
                      ReferenceOutputAssembly="false" />
  </ItemGroup>
</Project>

Important: Build the project before using OperationCodes. The source generator creates OperationCodes.g.cs at compile-time, so the class and its constants don’t exist until after a build. In microservices, you can generate the code in a shared service, package it as a NuGet, and let other services consume it safely.

Program.cs — use the generated constants:

using System;

namespace CodeGenDemo
{
    class Program
    {
        static void Main()
        {
            Console.WriteLine(OperationCodes.Processing);
            Console.WriteLine(OperationCodes.Completed);
            Console.WriteLine(OperationCodes.Cancelled);
        }
    }
}

Build & Verify

  1. Build the solution (Visual Studio or dotnet build).
  2. The generator runs and emits OperationCodes.g.cs under:
    CodeGenDemo/obj/Debug/net8.0/generated/CodeGenDemo.Generators/OperationCodes.g.cs
  3. Run the app (F5 / Ctrl+F5 or dotnet run --project CodeGenDemo).

Expected output:

1010001
1010002
1010003

Adapting to Your Data Source

Replace the hardcoded array in OperationCodeGenerator with a loader that reads from your chosen source of truth:

  • JSON/CSV file in the solution (easy, deterministic).
  • SQL via a pre-build step that exports a JSON file for the generator to read (generators should avoid runtime DB connections).

The generator consumes that file at compile time and regenerates constants automatically.

Benefits

  • No manual syncing between DB and code.
  • Compile-time safety with strongly typed constants.
  • Zero runtime overhead (all code is generated at build time).
  • Works across environments (CI, CLI, VS) with no extensions.

Conclusion

Roslyn Source Generators are the modern, reliable way to keep reference code mappings in sync across your .NET apps. Use a generator project targeting netstandard2.0, reference it as an analyzer from your app, and let the compiler create (and keep updating) your OperationCodes class automatically.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top