PhotoAtomic.IndentedStrings
0.6.0
dotnet add package PhotoAtomic.IndentedStrings --version 0.6.0
NuGet\Install-Package PhotoAtomic.IndentedStrings -Version 0.6.0
<PackageReference Include="PhotoAtomic.IndentedStrings" Version="0.6.0" />
<PackageVersion Include="PhotoAtomic.IndentedStrings" Version="0.6.0" />
<PackageReference Include="PhotoAtomic.IndentedStrings" />
paket add PhotoAtomic.IndentedStrings --version 0.6.0
#r "nuget: PhotoAtomic.IndentedStrings, 0.6.0"
#:package PhotoAtomic.IndentedStrings@0.6.0
#addin nuget:?package=PhotoAtomic.IndentedStrings&version=0.6.0
#tool nuget:?package=PhotoAtomic.IndentedStrings&version=0.6.0
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:
- Detects the indentation at each interpolation point by examining the whitespace immediately before
{{ - Applies that indentation to every line of the interpolated content
- 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
Keep each generation method at zero indentation
private static string GenerateProperty(string name) { return Indent($$""" public string {{name}} { get; set; } """); }Specify indentation at the interpolation point
return Indent($$""" class MyClass { {{GenerateProperty("Name")}} // ← Indented here } """);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
Don't hardcode indentation in the generated content
// ❌ BAD: Indentation baked into the string return Indent($$""" public string Name { get; set; } """);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());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:
- Always use
Indent($$"""...""")instead ofStringBuilder - Each generation method should return zero-indented content
- Apply indentation only at interpolation points by aligning
{{variable}}correctly in the template - For collections, use
Indent(collection.Select(...))directly - don't usestring.Join// ✅ DO THIS {{Indent(items.Select(i => $"case {i}: return true;"))}} // ❌ NOT THIS {{string.Join("\n", items.Select(i => $"case {i}: return true;"))}} - Remember: The handler looks at the whitespace before
{{to determine indentation level - 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 | Versions 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. |
-
.NETStandard 2.0
- System.Memory (>= 4.6.3)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.