Deterministic Output
Overview
Section titled “Overview”This is the final chapter of Part 2. When integrating all the symbol analysis, type extraction, and code generation techniques learned so far, one principle permeates the whole — the same input must always produce the same output. For the incremental caching covered in Chapter 4 to work correctly, this deterministic output must be guaranteed. If this principle is broken, incremental builds, source control, and CI/CD are all affected.
Learning Objectives
Section titled “Learning Objectives”Core Learning Objectives
Section titled “Core Learning Objectives”- Understanding the importance of deterministic output
- Impact on incremental builds, source control, and CI/CD
- Reason for using the global:: prefix
- Preventing namespace conflicts with user code
- Techniques for ensuring consistent output
- Excluding timestamps, sorting collections, consistent formatting
What Is Deterministic Output?
Section titled “What Is Deterministic Output?”Deterministic output means always producing the same output for the same input.
Deterministic Output====================Input A -> Output X (always)Input B -> Output Y (always)
Non-Deterministic Output========================Input A -> Output X (sometimes)Input A -> Output X' (other times)Why Is Deterministic Output Important?
Section titled “Why Is Deterministic Output Important?”1. Incremental Builds
Section titled “1. Incremental Builds”Deterministic Output====================Build 1: UserRepository -> UserRepositoryObservable.g.cs (content X)Build 2: UserRepository (no changes) -> Cache used (build skipped)
Non-Deterministic Output========================Build 1: UserRepository -> UserRepositoryObservable.g.cs (content X)Build 2: UserRepository (no changes) -> Content X' (different) -> Rebuild2. Source Control
Section titled “2. Source Control”Deterministic Output====================git status: No changes (generated files are identical)
Non-Deterministic Output========================git status: Files changed (content is semantically identical but different)-> Unnecessary commits occur3. Build Reproducibility
Section titled “3. Build Reproducibility”Deterministic Output====================Same results in any environment-> Improved CI/CD reliability
Non-Deterministic Output========================Different results depending on the environment-> "It worked on my machine..." problemglobal:: Prefix
Section titled “global:: Prefix”Why Is It Needed?
Section titled “Why Is It Needed?”// User codenamespace MyApp{ public class System { } // A class named System!}
// Generated code (without global::)namespace MyApp{ public class UserPipeline { System.ArgumentNullException.ThrowIfNull(x); // ❌ Error: MyApp.System has no ArgumentNullException! }}
// Generated code (with global::)namespace MyApp{ public class UserPipeline { global::System.ArgumentNullException.ThrowIfNull(x); // ✅ Correctly references the System namespace }}Always Use global::
Section titled “Always Use global::”// ✅ global:: prefix for all external typessb.AppendLine(" global::System.ArgumentNullException.ThrowIfNull(value);");sb.AppendLine(" global::System.Diagnostics.Activity.Current = null;");sb.AppendLine(" return global::LanguageExt.Unit.Default;");Using SymbolDisplayFormat
Section titled “Using SymbolDisplayFormat”// Using SymbolDisplayFormats.GlobalQualifiedFormatstring typeName = type.ToDisplayString(SymbolDisplayFormats.GlobalQualifiedFormat);// -> "global::System.Collections.Generic.List<global::MyApp.User>"
// Used in generated codesb.AppendLine($" {typeName} result = ...;");Eliminating Non-Deterministic Elements
Section titled “Eliminating Non-Deterministic Elements”1. Timestamps
Section titled “1. Timestamps”// ❌ Non-deterministicsb.AppendLine($"// Generated at {DateTime.Now}");
// ✅ Deterministicsb.AppendLine("// <auto-generated/>");2. GUIDs/Random Values
Section titled “2. GUIDs/Random Values”// ❌ Non-deterministicsb.AppendLine($"// ID: {Guid.NewGuid()}");
// ✅ Deterministic// GUID unnecessary - remove3. Environment Variables
Section titled “3. Environment Variables”// ❌ Non-deterministicstring path = Environment.GetEnvironmentVariable("PATH");sb.AppendLine($"// Path: {path}");
// ✅ Deterministic// Do not use environment variables4. Order Dependency
Section titled “4. Order Dependency”// ❌ Non-deterministic (order not guaranteed)var methods = classSymbol.GetMembers() .OfType<IMethodSymbol>() .ToList();
// ✅ Deterministic (sorted order)var methods = classSymbol.GetMembers() .OfType<IMethodSymbol>() .OrderBy(m => m.Name) // Sort by name .ThenBy(m => m.Parameters.Length) // Sort by parameter count .ToList();Consistent Formatting
Section titled “Consistent Formatting”Whitespace and Line Breaks
Section titled “Whitespace and Line Breaks”// ❌ Inconsistentsb.Append("public class ");sb.Append(className); // might sometimes use AppendLine
// ✅ Consistentsb.Append("public class ") .Append(className) .AppendLine() .AppendLine("{");Indentation
Section titled “Indentation”// ❌ Mixed (tabs and spaces)sb.AppendLine("\tprivate int _id;"); // tabsb.AppendLine(" private string _name;"); // spaces
// ✅ Consistent (spaces only)sb.AppendLine(" private int _id;");sb.AppendLine(" private string _name;");Verification Methods
Section titled “Verification Methods”Build Twice and Compare
Section titled “Build Twice and Compare”# First builddotnet buildcp Generated/MyClass.g.cs /tmp/first.cs
# Second build (without clean)dotnet buildcp Generated/MyClass.g.cs /tmp/second.cs
# Comparediff /tmp/first.cs /tmp/second.cs# There should be no differencesVerify with Tests
Section titled “Verify with Tests”[Fact]public void Generated_Code_Should_Be_Deterministic(){ string input = """ [GenerateObservablePort] public class UserRepository : IObservablePort { public FinT<IO, User> GetUserAsync(int id) => throw new(); } """;
// Generate twice string? output1 = _sut.Generate(input); string? output2 = _sut.Generate(input);
// Must be identical output1.ShouldBe(output2);}Verify Snapshot Tests
Section titled “Verify Snapshot Tests”[Fact]public Task Generated_Code_Should_Match_Snapshot(){ string input = """ [GenerateObservablePort] public class UserRepository : IObservablePort { public FinT<IO, User> GetUserAsync(int id) => throw new(); } """;
string? actual = _sut.Generate(input);
// Compare with snapshot return Verify(actual);}
// Result saved to .verified.txt file// If changed later, test fails -> check if the change was intentionalDeterministic Output Checklist
Section titled “Deterministic Output Checklist”□ Use global:: prefix for all external types□ Use SymbolDisplayFormat.FullyQualifiedFormat or custom format□ Exclude runtime values such as timestamps and GUIDs□ Do not use environment variables□ Sort collection order□ Consistent whitespace/indentation□ Verify identical results when building twiceSummary at a Glance
Section titled “Summary at a Glance”Here is a summary of the key principles for deterministic output.
| Element | Non-Deterministic | Deterministic |
|---|---|---|
| Type reference | System.Int32 | global::System.Int32 |
| Metadata | DateTime.Now | Excluded |
| Collections | Unordered | .OrderBy() |
| Whitespace | Mixed | Consistent rules |
| Verification | None | Snapshot tests |
Q1: What problems occur when deterministic output breaks?
Section titled “Q1: What problems occur when deterministic output breaks?”A: Three problems occur in cascade. First, cache is invalidated in incremental builds, causing a full rebuild every time. Second, meaningless diffs appear in source control, generating unnecessary commits. Third, the same code produces different results in CI/CD, breaking build reproducibility.
Q2: Does sorting with .OrderBy() affect performance?
Section titled “Q2: Does sorting with .OrderBy() affect performance?”A: Source generators run at compile time, so sorting cost is included in build time. Since the number of methods in most classes is a few dozen or fewer, the sorting cost is negligible. In fact, not sorting and causing non-deterministic output invalidates the incremental build cache, resulting in a much greater performance loss.
Q3: What is the difference between SymbolDisplayFormat and manually adding the global:: prefix?
Section titled “Q3: What is the difference between SymbolDisplayFormat and manually adding the global:: prefix?”A: Using SymbolDisplayFormat.FullyQualifiedFormat causes Roslyn to automatically generate the full path of a type including the global:: prefix. Manually hardcoding strings like "global::System.Int32" risks omission when types change. Functorium defines a custom SymbolDisplayFormats.GlobalQualifiedFormat to ensure consistency.
Part 2 has covered all the core concepts of source generators. In the next Part, we will learn how to handle complex cases encountered in practice, such as Primary Constructors, generic types, and collection types.