CodeWriter is the structured, allocation-conscious writer used to build generated C# source. Instead
of concatenating strings or writing raw text, generators describe what to emit — declarations,
statements, scopes — and the writer handles indentation, blank-line separation, generated attributes,
and deterministic layout.
This page uses the current best-practice API: bare semantic names (Class, Method, Property), the
minimal-parameter overloads with an optional configure callback, and structured statements
(Return, MethodCall, Assignment) instead of raw text.
Use the raw primitives for low-level text that has no structured equivalent:
writer.Write("partial"); // no trailing line feed
writer.Line("// generated"); // line feed appended
writer.Append("text"); // Write alias
writer.AppendLine("text"); // Line alias
writer.Comment("Explains the next member.");
writer.Indent(); // increase indentation
writer.NewLine();Write/Line/Append/AppendLine are the only methods that retain a verb prefix: everything
semantic drops it because the receiver is already a writer.
Each declaration writer has:
- a minimal overload taking name/type/accessibility plus an optional
configurecallback (options => options with { ... }); and - a scope form (
...Scope) returning aBlockScopeforusingwhen you need fine-grained control.
writer.Class(
"OrderService",
TypeDeclarationAccessibility.Public,
options => options with { IsSealed = true, IsPartial = false },
body =>
{
body.Field("_total", TypeIdentity.Create<decimal>().AsTypeReference(), TypeDeclarationAccessibility.Private);
body.Constructor(
"OrderService",
TypeDeclarationAccessibility.Public,
options => options with
{
Parameters = [new("total", TypeIdentity.Create<decimal>().AsTypeReference())],
},
constructorBody => constructorBody.Assignment("_total", "total")
);
body.Property(
"Total",
TypeIdentity.Create<decimal>().AsTypeReference(),
TypeDeclarationAccessibility.Public
);
}
);The same pattern applies to Struct, RecordClass, RecordStruct, Interface, Enum (+ EnumField),
Type (kind-driven), Delegate, AttributeClass, Method/PartialMethod/MethodExpression,
Property/PropertyExpression, Indexer, Field, and Operator.
using (writer.ClassScope("OrderService", TypeDeclarationAccessibility.Public))
using (writer.MethodScope("Apply", PurviewTypeLibrary.System.Void, TypeDeclarationAccessibility.Public))
{
writer.MethodCall("Validate");
}Scope forms are ideal when a declaration spans multiple calls, loops, or conditional content. The
using statement is mandatory — the closing token and indentation are written on dispose, and the
DiscardedCodeWriterScopeAnalyzer (PSGFR17) flags scope returns that are dropped.
Emit executable statements through the structured statement methods rather than raw Line:
writer.MethodCall("Process", "item"); // Process(item);
writer.AwaitedMethodCall("SaveAsync", "cancellationToken"); // await SaveAsync(cancellationToken);
writer.MethodCallOn("variable", "Process", "item"); // variable.Process(item);
writer.AwaitedMethodCallOn("service", "LoadAsync", "token"); // await service.LoadAsync(token);
writer.Return("value"); // return value;
writer.Throw(TypeIdentity.Create<InvalidOperationException>(), "Failed."); // throw new ...;
writer.Assignment("_total", "value"); // _total = value;
writer.IfBlock("value is null", body => body.Return("null"));
writer.IfBlock("value is null", body => body.Return("null"))
.ElseIf("value is 0", body => body.Return("zero"))
.Else(body => body.Return("value"));
writer.Foreach("var item in items", body => body.MethodCallOn("item", "Process"));MethodCall/AwaitedMethodCall write a call without a receiver — Process(item); or
await SaveAsync(token);. Use MethodCallOn/AwaitedMethodCallOn (or the receiver parameter on the
IEnumerable overloads) for a call on a variable, including generic arguments:
writer.MethodCall("Create", ["x"], receiver: "factory", genericArguments: [TypeReference.Create<string>()]);
// factory.Create<string>(x);A chained invocation — where the result of each call is the receiver of the next, and a postfix is
applied to the final result — is expressed with MethodCallChain/AwaitedMethodCallChain. The chain
is written as an expression (no terminating semicolon), so it composes as the value of an
Assignment/Return expression callback:
writer.Assignment(
"var hostKitOptions",
expression => expression.MethodCallChain(
"builder.Configuration.GetSection",
[$"{name}.SectionName"],
chain => chain.Method("Get", genericArguments: [optionsType]).Postfix(" ?? new()")));
// var hostKitOptions = builder.Configuration.GetSection("x.SectionName").Get<Options>() ?? new();rootMethodmay include the receiver (e.g.builder.Configuration.GetSection); each subsequent.Method(...)call implicitly uses the previous result as its receiver.genericArgumentsprovides the<...>type arguments for a segment.Postfix(expression)appends a trailing expression such as?? new()or!.
A null-conditional receiver — onBuilt?.Invoke(this, builder); — is written with the nullConditional
argument on the structured MethodCallOn/AwaitedMethodCallOn overloads:
writer.MethodCallOn("onBuilt", "Invoke", ["this", "builder"], nullConditional: true);
// onBuilt?.Invoke(this, builder);IfBlock/IfBlockScope write an if block. ElseIf/ElseIfScope chain an else if block after an
if or another else if, and Else/ElseScope close the chain with an else block. The methods
return the writer, so branches can be chained fluently:
writer
.IfBlock("value is null", body => body.Return("null"))
.ElseIf("value is 0", body => body.Return("zero"))
.Else(body => body.Return("value"));Emits:
if (value is null)
{
return null;
}
else if (value is 0)
{
return zero;
}
else
{
return value;
}IfElse(condition, ifBody, elseBody) is the compact two-branch form. The scope forms
IfBlockScope, ElseIfScope, and ElseScope write the header and return the body scope for
content that spans multiple calls.
HashDefines/HashDefinesScope write a #if/#endif block with both directives at column zero.
The body keeps the surrounding indentation — file-level directives and their content stay at column
zero, while class members inside the block stay at the same indent as their siblings:
using (writer.HashDefinesScope("!EXCLUDE_PURVIEW_TELEMETRY_LOGGING"))
{
writer.FileScopedNamespace("Example");
writer.Enum("Mode", TypeDeclarationAccessibility.Public, fields: [new("Default", 0)]);
}
// Equivalent action form:
writer.HashDefines("NET", body => body.Line("// NET only"));Emits:
#if !EXCLUDE_PURVIEW_TELEMETRY_LOGGING
namespace Example;
...
#endifAt file level these blocks are self-spacing: a blank line is ensured before the #if and after the
#endif, so directive sections remain separated without explicit NewLine() calls.
HashElse() writes the #else directive at column zero between the two bodies:
using (writer.HashDefinesScope("NET48_OR_GREATER || PURVIEW_TELEMETRY_NON_NULLABLE"))
{
writer.Property("name", TypeIdentity.Create<string>().AsTypeReference(), TypeDeclarationAccessibility.Public,
options => options with { HasSetter = true, IncludeGeneratedAttributes = false });
writer.HashElse();
writer.Property("name", TypeIdentity.Create<string>().MakeNullable(writer), TypeDeclarationAccessibility.Public,
options => options with { HasSetter = true, IncludeGeneratedAttributes = false });
}Emits:
#if NET48_OR_GREATER || PURVIEW_TELEMETRY_NON_NULLABLE
public string name { get; set; }
#else
public string? name { get; set; }
#endifEmptyScope() returns a no-op scope so a block can be wrapped only when a guard requires it:
using var scope = wrapInExcludeLoggingGuard
? writer.EmptyScope()
: writer.HashDefinesScope("EXCLUDE_PURVIEW_TELEMETRY_LOGGING");PragmaDisable writes a single #pragma warning disable directive at column zero for one or more
warning codes. At file level it is self-spacing (blank lines are ensured around the directive):
writer.PragmaDisable("CS8625", "CS0618");
// #pragma warning disable CS8625 CS0618For a scoped disable that restores the warnings when the scope is disposed, use OpenPragmasScope:
using (writer.OpenPragmasScope("CS0618"))
{
writer.Line("ObsoleteCall();");
}
// #pragma warning disable CS0618
// ObsoleteCall();
// #pragma warning restore CS0618The full header pattern — nullable directive, conditional #nullable enable, and a disabled warning —
can be expressed entirely through the structured APIs (the file-level directives are self-spacing, so
no explicit NewLine() calls are needed):
writer.AutoGeneratedHeader(nullableDirective: NullableDirectiveMode.Disable);
writer.HashDefines("!NET48_OR_GREATER && !PURVIEW_TELEMETRY_NON_NULLABLE", hashWriter => hashWriter.Line("#nullable enable"));
writer.PragmaDisable("CS8625");
writer.FileScopedNamespace("Purview.Telemetry");Emits:
// <auto-generated />
// This code was generated by ExampleGenerator (version 1.0.0).
// Changes to this file will be lost when the source generator runs again.
#if !NET48_OR_GREATER && !PURVIEW_TELEMETRY_NON_NULLABLE
#nullable enable
#endif
#pragma warning disable CS8625
namespace Purview.Telemetry;NetConditionalReturn writes a return for an interpolated string using the best invariant-culture
API on each target framework, guarded by #if NET:
writer.Method(
"Format",
TypeIdentity.Create<string>().AsTypeReference(),
TypeDeclarationAccessibility.Public,
null,
body => body.NetConditionalReturn("Value: {_value}")
);Emits:
#if NET
return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"Value: {_value}");
#else
return global::System.FormattableString.Invariant($"Value: {_value}");
#endifCodeWriter applies a default accessibility for each member kind when a declaration does not specify
one. Set the defaults on GenerationSettings (to apply across a generation) or on the writer itself
(to override per writer). Each value is null-able, so setting a kind back to null omits the
modifier entirely.
| Setting | Default |
|---|---|
DefaultTypeAccessibility |
Public |
DefaultPropertyAccessibility |
Public |
DefaultPropertyGetterAccessibility |
Public |
DefaultPropertySetterAccessibility |
Public |
DefaultFieldAccessibility |
Private |
DefaultMethodAccessibility |
Public |
DefaultConstructorAccessibility |
Public |
DefaultIndexerAccessibility |
Public |
DefaultOperatorAccessibility |
Public |
var writer = generationContext.CreateCodeWriter();
writer.Field("_total", TypeReference.Create<decimal>()); // private int _total; (DefaultFieldAccessibility)
writer.Property("Total", TypeReference.Create<decimal>()); // public decimal Total { get; }An explicit accessibility always wins over the default:
writer.Property("Total", TypeReference.Create<decimal>(), TypeDeclarationAccessibility.Internal);
// internal decimal Total { get; }Accessor (getter/setter) defaults are emitted only when they are more restrictive than the
property's own accessibility — C# forbids an accessor modifier that is equal to or more permissive
than the property (CS0273). With the public defaults, a public property keeps bare { get; set; }:
writer.DefaultPropertySetterAccessibility = TypeDeclarationAccessibility.Private;
writer.Property("Name", TypeReference.Create<string>(), TypeDeclarationAccessibility.Public,
options => options with { HasSetter = true });
// public string Name { get; private set; }- Prefer the minimal overloads with a
configurecallback over constructing*DeclarationOptionsvalues manually — thePreferMinimalCodeWriterOverloadAnalyzer(PSGFR20) flags the verbose form. - Prefer structured declarations and statements over raw text —
PreferStructuredCodeWriterApiAnalyzer(PSGFR18) andPreferStructuredCodeWriterStatementAnalyzer(PSGFR19) flag raw emission. - Prefer
IfBlock/ElseIf/Elseover generic block methods for conditional content — thePreferStructuredCodeWriterIfBlockAnalyzer(PSGFR23) flagsOpenBlockScope/OpenBlockheaders that write anif,else if, orelsestatement, and its code fix rewrites them. - Always consume scope-returning methods with
using(PSGFR17). - Keep every value emitted through the structured API so layout stays deterministic and the analyzers can guide callers back to the best practice.
The SourceGeneratorFramework.ExampleGenerator
reference implementation demonstrates these APIs end-to-end, including the CodeWriterSampleGenerator,
which compiles a best-practice sample class for every [GenerateCodeWriterSample] target.