Skip to content

A comparison between Microsoft’s source generator implementation using StringBuilder and an alternative built with Nest.

Notifications You must be signed in to change notification settings

h-shahzaib/NestVsMsLogger

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

I reimplemented a source generator originally built by Microsoft. It was made for high-performance logging scenarios—very optimized, but also a bit dense in terms of how the code was structured. So I rebuilt it using my own library, Nest, which I’ve been working on for structured code generation.

The goal wasn’t to change what it does—it still produces the same output—but to explore how it can be done in a more composable and maintainable way. Nest gives me a much better mental model for building generators, especially when you’re juggling indentation, conditionals, and nesting.

Here’s a breakdown of how both implementations approach things differently.


🧱 1. Structured Text Generation vs Manual String Building

❌ Microsoft’s Original Version (Using StringBuilder)

_builder.Append($@"
            {nestedIndentation}public override string ToString()
            {nestedIndentation}{{
");
GenVariableAssignments(lm, nestedIndentation);
string formatMethodBegin = !lm.Message.Contains('{') ? "" :
    _hasStringCreate ? "string.Create(global::System.Globalization.CultureInfo.InvariantCulture, " :
    "global::System.Diagnostics.CodeAnalysis.FormattableString.Invariant(";
string formatMethodEnd = formatMethodBegin.Length > 0 ? ")" : "";
_builder.Append($@"
                {nestedIndentation}return {formatMethodBegin}$""{ConvertEndOfLineAndQuotationCharactersToEscapeForm(lm.Message)}""{formatMethodEnd};
            {nestedIndentation}}}
");

Manual indentation tracking ({nestedIndentation})
Formatting and logic mixed together
Easy to break formatting if you're not constantly watching spacing and newlines

✅ My Version (Using Nest)

b.L("public override string ToString()").B(b =>
{
    VariableAssignmentsForToString(b, lm);

    if (ConvertEndOfLineAndQuotationCharactersToEscapeForm(lm.Message) is string escaped_msg)
    {
        b.L();

        if (!lm.Message.Contains('{'))
            b.L($"return `{escaped_msg}`;");
        else if (m_HasStringCreate)
            b.L($"return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $`{escaped_msg}`);");
        else
            b.L($"return global::System.Diagnostics.CodeAnalysis.FormattableString.Invariant($`{escaped_msg}`);");
    }
});

Nest handles indentation and braces automatically
Logic stays clean and scoped


🔁 2. Reusable Nesting via Function Composition

❌ Microsoft’s Version: Hardcoded Loop + Manual Indentation

LoggerClass parent = lc.ParentClass;
var parentClasses = new List<string>();
while (parent != null)
{
    parentClasses.Add($"partial {parent.Keyword} {parent.Name}");
    parent = parent.ParentClass;
}

for (int i = parentClasses.Count - 1; i >= 0; i--)
{
    _builder.Append($@"
{nestedIndentation}{parentClasses[i]}
{nestedIndentation}{{");
    nestedIndentation += "    ";
}

_builder.Append($@"
{nestedIndentation}partial {lc.Keyword} {lc.Name}
{nestedIndentation}{{");

Manual depth tracking
Every nesting level updates the indentation string
Not reusable outside of this specific case

✅ Nest Version: Function-Chained Nesting

var logic = (ITextBuilder b) =>
{
    b.L($"partial {keyword} {name}").B(b =>
    {
        GenerateLogger(b, lc);
    });
};

var parent = lc.ParentClass;

while (parent != null)
{
    logic = b =>
    {
        b.L($"partial {keyword} {name}")
            .B(logic);
    };

    parent = parent.ParentClass;
}

Build your logic as nested functions
No need to manually manage indentation
Works the same whether you’re 1 level deep or 10


📄 3. Mixing Static + Dynamic Code is Easy

In some cases, you want to drop in a block of static C#—like a helper class—but keep everything flowing through the same system.

✅ Nest makes that simple

if (m_NeedEnumerationHelper)
    GenerateEnumerationHelper(b);

And GenerateEnumerationHelper just uses raw string literals:

b.L(
    $$$"""
    /// <summary> ... </summary>
    [GeneratedCode(...)]

    internal static class __LoggerMessageGenerator
    {
        public static string Enumerate(global::System.Collections.IEnumerable? enumerable)
        {
            // ...
        }
    }
    """
);

Just paste raw C# as-is
Still fits into the Nest pipeline without doing anything special


🔄 4. Composable Helpers for Repeated Logic

For example, assigning variables inside the generated ToString() is something I extracted out into a reusable function:

✅ Nest version

private void VariableAssignmentsForToString(ITextBuilder b, LoggerMethod lm)
{
    foreach (KeyValuePair<string, string> t in lm.TemplateMap)
    {
        int index = 0;
        foreach (LoggerParameter p in lm.TemplateParameters)
        {
            ReadOnlySpan<char> template = RemoveSpecialSymbol(t.Key.AsSpan());
            ReadOnlySpan<char> parameter = RemoveSpecialSymbol(p.Name.AsSpan());
            if (template.Equals(parameter, StringComparison.OrdinalIgnoreCase))
                break;
            index++;
        }

        if (index < lm.TemplateParameters.Count)
        {
            if (lm.TemplateParameters[index].IsEnumerable)
            {
                b.L($"var {t.Key} = global::__LoggerMessageGenerator.Enumerate((global::System.Collections.IEnumerable ?)this.{NormalizeSpecialSymbol(lm.TemplateParameters[index].CodeName)});");
                
                m_NeedEnumerationHelper = true;
            }
            else
            {
                b.L($"var {t.Key} = this.{NormalizeSpecialSymbol(lm.TemplateParameters[index].CodeName)};");
            }
        }
    }
}

Self-contained
Easy to test or reuse elsewhere
Doesn’t care where it gets used — just needs a builder and some data

❌ Microsoft’s version

private void GenVariableAssignments(LoggerMethod lm, string nestedIndentation)
{
    // Same logic but tied to _builder and manual indentation
}

The logic here is embedded directly in the context and tightly coupled to _builder, a manually tracked nestedIndentation, and hardcoded indentation baked right into the strings within the method itself.

That makes testing tricky — if you want to assert the generated output, your test needs to match the exact spacing and formatting. Even a single space off and the test fails. So now, instead of focusing on just the logic, you're also forced to replicate how indentation was being managed elsewhere in the generator.


🧩 Final Thoughts

Nest doesn’t magically do anything you couldn’t already do with StringBuilder. But it lets you structure your output like a real system—made of small, composable pieces, cleanly separated logic, and reusable helpers.

You can mix in raw strings, conditionals, and nested blocks without stressing over indentation or formatting quirks.

And just to be clear: both the original and the Nest-based generator produce the same output. You can check the Output/ folder for examples. I’ve added a bunch of test cases and snapshots to confirm this—aside from a few minor whitespace or line break differences, the output is completely identical.

About

A comparison between Microsoft’s source generator implementation using StringBuilder and an alternative built with Nest.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages