PhotoAtomic.IndentedStrings 0.6.0

dotnet add package PhotoAtomic.IndentedStrings --version 0.6.0
                    
NuGet\Install-Package PhotoAtomic.IndentedStrings -Version 0.6.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PhotoAtomic.IndentedStrings" Version="0.6.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PhotoAtomic.IndentedStrings" Version="0.6.0" />
                    
Directory.Packages.props
<PackageReference Include="PhotoAtomic.IndentedStrings" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PhotoAtomic.IndentedStrings --version 0.6.0
                    
#r "nuget: PhotoAtomic.IndentedStrings, 0.6.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PhotoAtomic.IndentedStrings@0.6.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PhotoAtomic.IndentedStrings&version=0.6.0
                    
Install as a Cake Addin
#tool nuget:?package=PhotoAtomic.IndentedStrings&version=0.6.0
                    
Install as a Cake Tool

IndentedInterpolatedStringHandler

A custom interpolated string handler for C# that automatically manages indentation when composing multi-line strings, particularly useful for source code generators.

Why Use This Instead of StringBuilder?

Traditional Approach (StringBuilder)

private static string GenerateClass(string className)
{
    var sb = new StringBuilder();
    sb.AppendLine("public class " + className);
    sb.AppendLine("{");
    sb.AppendLine("    public string Name { get; set; }");
    sb.AppendLine("    ");
    sb.AppendLine("    public void Method()");
    sb.AppendLine("    {");
    sb.AppendLine("        Console.WriteLine(\"Hello\");");
    sb.AppendLine("    }");
    sb.AppendLine("}");
    return sb.ToString();
}

Problems:

  • Hard to visualize the final output structure
  • Indentation is hardcoded in strings
  • Error-prone (easy to miss spaces or tabs)
  • Not composable (difficult to insert generated blocks)

Modern Approach (IndentedInterpolatedStringHandler)

private static string GenerateClass(string className)
{
    return Indent($$"""
        public class {{className}}
        {
            public string Name { get; set; }
            
            public void Method()
            {
                Console.WriteLine("Hello");
            }
        }
        """);
}

Benefits:

  • Code structure is immediately visible
  • Natural C# raw string literal syntax
  • Automatic indentation management
  • Composable blocks

Core Concept: Zero-Based Indentation

The key principle is that each code generation method returns content with minimal (zero-based) indentation. The IndentedInterpolatedStringHandler automatically applies the correct indentation based on where the content is interpolated.

The Pattern

// ❌ WRONG: Indentation hardcoded in the generated content
private static string GenerateMethod()
{
    return Indent($$"""
            public void MyMethod()
            {
                Console.WriteLine("Test");
            }
        """);
}

// ✅ CORRECT: Zero-based indentation
private static string GenerateMethod()
{
    return Indent($$"""
        public void MyMethod()
        {
            Console.WriteLine("Test");
        }
        """);
}

Composing Blocks

When composing multiple blocks, indentation is specified at the interpolation point:

private static string GenerateClass(string className)
{
    var methods = GenerateMethods();  // Returns zero-indented content
    var properties = GenerateProperties();  // Returns zero-indented content
    
    return Indent($$"""
        public class {{className}}
        {
            {{properties}}    // ← Indented at class member level
            
            {{methods}}       // ← Indented at class member level
        }
        """);
}

private static string GenerateMethods()
{
    // Returns content at "zero" indentation level
    return Indent($$"""
        public void Method1()
        {
            Console.WriteLine("Method 1");
        }
        
        public void Method2()
        {
            Console.WriteLine("Method 2");
        }
        """);
}

Key Point: The interpolation {{methods}} is indented to the correct level in the template, and the handler automatically indents every line of the interpolated content to match that level.

Real-World Example: Source Generator

Here's a practical example from a clone generator that demonstrates the pattern:

private static string GenerateCloneExtensions(ClassInfo classInfo)
{
    // Generate individual pieces with zero-based indentation
    var simpleClone = GenerateSimpleClone(classInfo);
    var trackedClone = GenerateTrackedClone(classInfo);
    var helperSetter = GenerateHelperSetter(classInfo);
    
    // Compose them with proper indentation at interpolation points
    return Indent($$"""
        // <auto-generated/>
        #nullable enable
        using System;
        using System.Linq;
        
        namespace {{classInfo.Namespace}}
        {
            /// <summary>
            /// Extension methods for cloning {{classInfo.ClassName}}
            /// </summary>
            public static partial class {{classInfo.ClassName}}Extensions
            {
                {{simpleClone}}      // ← Indented at class member level
        
                {{trackedClone}}     // ← Indented at class member level
        
                private static T ThrowHelper<T>() => throw new Exception();
            }
            {{helperSetter}}         // ← Indented at namespace level
        }
        """);
}

private static string GenerateSimpleClone(ClassInfo classInfo)
{
    // Zero-based indentation - no leading spaces
    return Indent($$"""
        /// <summary>
        /// Creates a deep clone of the {{classInfo.ClassName}}.
        /// </summary>
        public static {{classInfo.FullyQualifiedName}}? Clone(this {{classInfo.FullyQualifiedName}}? source)
        {
            if (source == null) return null;
            return source.Clone(new CloneContext());
        }
        """);
}

private static string GenerateTrackedClone(ClassInfo classInfo)
{
    // ✅ BEST: Pass IEnumerable<string> directly to Indent
    return Indent($$"""
        /// <summary>
        /// Creates a deep clone with reference tracking.
        /// </summary>
        public static {{classInfo.FullyQualifiedName}}? Clone(
            this {{classInfo.FullyQualifiedName}}? source,
            CloneContext context)
        {
            if (source == null) return null;
            
            return context.GetOrClone(
                source,
                shellFactory: () => new {{classInfo.FullyQualifiedName}}(),
                populateAction: (clone, ctx) =>
                {
                    {{Indent(classInfo.Properties.Select(p => 
                        $"clone.{p.Name} = source.{p.Name};"))}}
                });
        }
        """);
}

private static string? GenerateHelperSetter(ClassInfo classInfo)
{
    if (!classInfo.NeedsHelper) return null;
    
    // ✅ Multi-line blocks per item also work great
    return Indent($$"""
        
        public partial class {{classInfo.ClassName}}
        {
            internal void __SetProperties(
                {{Indent(classInfo.Properties.Select(p => $"{p.Type} {p.Name.ToLower()}"))}}
            )
            {
                {{Indent(classInfo.Properties.Select(p => 
                    $"this.{p.Name} = {p.Name.ToLower()};"))}}
            }
        }
        """);
}

// ✅ Complex example: Generate validation logic for each property
private static string GenerateValidator(ClassInfo classInfo)
{
    return Indent($$"""
        public void Validate()
        {
            {{Indent(classInfo.Properties
                .Where(p => p.IsRequired)
                .Select(p => $$"""
                    if ({{p.Name}} == null)
                        throw new ValidationException("{{p.Name}} is required");
                    """))}}
        }
        """);
}

How It Works

The IndentedInterpolatedStringHandler:

  1. Detects the indentation at each interpolation point by examining the whitespace immediately before {{
  2. Applies that indentation to every line of the interpolated content
  3. Preserves relative indentation within the interpolated content

Example Flow

var method = Indent($$"""
    public void Test()
    {
        Console.WriteLine("Hi");
    }
    """);

var result = Indent($$"""
    class MyClass
    {
        {{method}}    // ← 4 spaces of indentation here
    }
    """);

Result:

class MyClass
{
    public void Test()          // ← 4 spaces added
    {                           // ← 4 spaces added
        Console.WriteLine("Hi");// ← 4 spaces added (8 total)
    }                           // ← 4 spaces added
}

Best Practices

✅ DO

  1. Keep each generation method at zero indentation

    private static string GenerateProperty(string name)
    {
        return Indent($$"""
            public string {{name}} { get; set; }
            """);
    }
    
  2. Specify indentation at the interpolation point

    return Indent($$"""
        class MyClass
        {
            {{GenerateProperty("Name")}}    // ← Indented here
        }
        """);
    
  3. Use Indent(IEnumerable<string>) for collections - no need for string.Join

var properties = new[] { "Name", "Age", "Email" };
   
// ✅ BEST: Let Indent handle the collection directly
return Indent($$"""
    public class Person
    {
        {{Indent(properties.Select(p => $"public string {p} {{ get; set; }}"))}}
    }
    """);
   
// ✅ ALSO GOOD: For multi-line blocks per item
return Indent($$"""
    public void Initialize()
    {
        {{Indent(properties.Select(p => $$"""
            if ({{p}} == null)
                throw new ArgumentNullException(nameof({{p}}));
            """))}}
    }
    """);
   
// ❌ AVOID: Manually using string.Join (unnecessary)
var joined = string.Join("\n", properties.Select(p => $"public string {p} {{ get; set; }}"));
return Indent($$"""
    public class Person
    {
        {{joined}}
    }
    """);

Why this works: Indent() has an overload that accepts IEnumerable<string> and automatically:

  • Concatenates all strings
  • Applies the correct indentation to each line
  • Handles multi-line strings within the collection

❌ DON'T

  1. Don't hardcode indentation in the generated content

    // ❌ BAD: Indentation baked into the string
    return Indent($$"""
                public string Name { get; set; }
        """);
    
  2. Don't mix StringBuilder and Indent unnecessarily

    // ❌ BAD: Using StringBuilder defeats the purpose
    var sb = new StringBuilder();
    sb.AppendLine("public class Test");
    sb.AppendLine("{");
    return Indent(sb.ToString());
    
  3. Don't forget to indent at interpolation points

    // ❌ BAD: Missing indentation at interpolation
    return Indent($$"""
        class MyClass
        {
        {{GenerateMethod()}}    // ← Should be indented!
        }
        """);
    

Usage in Your Project

using static PhotoAtomic.IndentedStrings.IndentedInterpolatedStringHandler;

public class MyGenerator : IIncrementalGenerator
{
    private static string GenerateCode(ClassInfo info)
    {
        return Indent($$"""
            // Generated code for {{info.Name}}
            public class {{info.Name}}
            {
                // Class content here
            }
            """);
    }
}

Technical Details

  • Namespace: PhotoAtomic.IndentedStrings
  • Main Type: IndentedInterpolatedStringHandler
  • Entry Point: Indent() static method
  • Requires: C# 11+ (for raw string literals and interpolated string handlers)

For AI Coding Agents

When generating source code with this tool:

  1. Always use Indent($$"""...""") instead of StringBuilder
  2. Each generation method should return zero-indented content
  3. Apply indentation only at interpolation points by aligning {{variable}} correctly in the template
  4. For collections, use Indent(collection.Select(...)) directly - don't use string.Join
    // ✅ DO THIS
    {{Indent(items.Select(i => $"case {i}: return true;"))}}
    
    // ❌ NOT THIS
    {{string.Join("\n", items.Select(i => $"case {i}: return true;"))}}
    
  5. Remember: The handler looks at the whitespace before {{ to determine indentation level
  6. Multi-line blocks in collections work perfectly:
    {{Indent(properties.Select(p => $$"""
        if ({{p.Name}} == null)
            throw new Exception("{{p.Name}} is required");
        """))}}
    

This approach makes generated code more maintainable, readable, and less error-prone.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.6.0 48 8/23/2026
0.5.0 93 8/16/2026
0.4.0 87 8/16/2026
0.3.0 86 8/15/2026
0.2.1 85 8/12/2026
0.2.0 83 8/12/2026
0.1.0 84 8/12/2026