CSharpAuthor 2.0.0

dotnet add package CSharpAuthor --version 2.0.0
                    
NuGet\Install-Package CSharpAuthor -Version 2.0.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="CSharpAuthor" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CSharpAuthor" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="CSharpAuthor" />
                    
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 CSharpAuthor --version 2.0.0
                    
#r "nuget: CSharpAuthor, 2.0.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 CSharpAuthor@2.0.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=CSharpAuthor&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=CSharpAuthor&version=2.0.0
                    
Install as a Cake Tool

CSharpAuthor

Programmatically generate C# source. Built for Roslyn source generators, where it is about 25× faster than SyntaxFactory + NormalizeWhitespace — 0.019 ms against 0.489 ms per file, measured on the same machine in the same run.

You build a tree of definitions and expressions; it emits formatted C#. Nothing is a string until serialization — types stay unrendered, so namespaces are derived from what you actually wrote, and expressions stay structured, so precedence and escaping are handled for you.

using CSharpAuthor;
using CSharpAuthor.Expressions;

var file = new CSharpFileDefinition("Sample.Generated");

var widget = file.AddClass("Widget");
widget.Modifiers = ComponentModifier.Public | ComponentModifier.Partial;

var name = widget.AddProperty(typeof(string), "Name");
name.Modifiers = ComponentModifier.Public;

var describe = widget.AddMethod("Describe");
describe.Modifiers = ComponentModifier.Public;
describe.SetReturnType(typeof(string));
describe.Return(Ex.Interpolate("widget ", Ex.Id("Name")));

var rank = widget.AddMethod("Rank");
rank.Modifiers = ComponentModifier.Public;
rank.SetReturnType(typeof(int));
rank.Return(Ex.Switch(Ex.Id("Name"),
    Ex.Arm(Pat.Null, Ex.Int(0)),
    Ex.Arm(Pat.Declaration(TypeDefinition.Get(typeof(string)), "s"),
           Ex.Id("s").Dot("Length").Is(Pat.GreaterThan(Ex.Int(8))),
           Ex.Int(2)),
    Ex.Arm(Pat.Discard, Ex.Int(1))));

var context = new OutputContext();

file.WriteOutput(context);

var outputString = context.Output();
namespace Sample.Generated
{
    public partial class Widget
    {

        public string Name { get; set; }

        public string Describe()
        {
            return $"widget {Name}";
        }

        public int Rank()
        {
            return Name switch
            {
                null => 0,
                string s when s.Length is > 8 => 2,
                _ => 1
            };
        }
    }
}

Nothing in that input was a fragment of C# text. Ex.Interpolate decided where the $"…{…}" holes go; Ex.Switch laid out the arms and indented them; Pat built the guard. That is the library working as intended, and it is what the rest of this page is about.

Install

dotnet add package CSharpAuthor

CSharpAuthor ships as source, compiled into your project, so a source generator can use it without taking on a dependency it would then have to redistribute. In a generator project:

<PackageReference Include="CSharpAuthor" Version="2.0.0">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>build</IncludeAssets>
</PackageReference>
<PropertyGroup>
  <PackageCSharpAuthorIncludeSource>true</PackageCSharpAuthorIncludeSource>
</PropertyGroup>

IncludeAssets="build" is what keeps the package's assembly out of your reference list, so the source compiled into your project is the only copy of each type. If you leave it off, the package drops the reference itself — the two cannot both apply — so a plain PackageReference works too.

An optional Roslyn bridge — ITypeSymbol → ITypeDefinition, and attribute reading — ships in the same package behind PackageCSharpAuthorIncludeRoslyn=true. Your generator already references Roslyn, so it costs nothing extra. It implies PackageCSharpAuthorIncludeSource.

Why the type model matters

ITypeDefinition is not a string. It stays unrendered until serialization, which is what lets one option flip an entire file between short names and global::, resolve same-name collisions with an alias, and make a missing using structurally impossible.

var options = new OutputContextOptions { TypeOutputMode = TypeOutputMode.Global };
var context = new OutputContext(options);

Global qualifies every reference and emits no derived usings — recommended for a generator, and the faster path, because nothing you emit can collide with anything in the consumer's file. ShortName gives you shorter names, derived using directives and automatic collision aliasing.

ShortName is the default, so a generator has to opt in to the mode it wants.

Building statements

Statements are built the same way types are: out of objects that stay unrendered until the end. Ex builds expressions, Pat builds patterns. Both live in CSharpAuthor.Expressions.

using CSharpAuthor.Expressions;

Expressions — Ex

Ex.Id is an identifier, Ex.Str a string literal, and the difference matters:

Ex.Id("Name")       // Name        — an identifier, keyword-escaped
Ex.Str("Name")      // "Name"      — a string literal, with escaping
Ex.Int(42)          // 42
Ex.Value(1.5m)      // 1.5M        — the suffix, because `decimal d = 1.5;` is CS0664

Identifiers are escaped for you, which is the whole reason to build them as objects:

Ex.Id("class")                        // @class
Ex.On(targetsType, "new")             // AttributeTargets.@new

Member access, calls and null-conditionals chain:

Ex.Id("sb").Call("Append", Ex.Str("hi"))          // sb.Append("hi")
Ex.Id("sb").NullCall("Clear")                     // sb?.Clear()
Ex.New(TypeDefinition.Get(typeof(StringBuilder))) // new StringBuilder()
Ex.Id("items").Call("Select", Ex.Lambda("x", Ex.Id("x").Dot("Name")))
                                                  // items.Select(x => x.Name)

Precedence is handled, so you never parenthesise by hand. Ex knows where each operator sits and brackets only what needs it:

Ex.Multiply(Ex.Add(Ex.Int(1), Ex.Int(2)), Ex.Int(3))   // (1 + 2) * 3
Ex.Add(Ex.Int(1), Ex.Multiply(Ex.Int(2), Ex.Int(3)))   //  1 + 2 * 3

One trap worth knowing: & and | on Ex are the short-circuiting operators, because that is what generated code usually wants. For a [Flags] combination you want Ex.BitOr, not |:

Ex.On(t, "Class") | Ex.On(t, "Struct")             // AttributeTargets.Class || …   ⚠ CS0019
Ex.BitOr(Ex.On(t, "Class"), Ex.On(t, "Struct"))    // AttributeTargets.Class | …

Patterns — Pat

Every pattern form C# has, including the combinators. Patterns are used through Ex.Is, or as switch arms:

Ex.Id("value").Is(Pat.NotNull())                      // value is not null
Ex.Id("value").Is(Pat.Declaration(stringType, "s"))   // value is string s
Ex.Id("value").Is(Pat.GreaterThan(Ex.Int(8)))         // value is > 8

Ex.Id("value").Is(Pat.Or(Pat.Constant(Ex.Int(1)),
                         Pat.Constant(Ex.Int(2))))    // value is 1 or 2

Pat.Null, Pat.Discard, Pat.Type, Pat.Var, Pat.VarTuple, Pat.Relational, Pat.Not, Pat.And and property patterns via Pat.Prop cover the rest.

Putting them in a method

An Ex goes into a method body through the statement API — there is no template step:

method.Assign(Ex.New(sbType)).ToVar("sb");        // var sb = new StringBuilder();
method.AddIndentedStatement(Ex.Id("sb").Call("Append", Ex.Str("hi")));
method.Return(Ex.Id("sb").Call("ToString"));      // return sb.ToString();

var block = method.If(Ex.Id("sb").Dot("Length").Is(Pat.GreaterThan(Ex.Int(0))));
block.Return(null);                               // if (sb.Length is > 0) { return; }

Blocks

If, ForEach, For, While and Lock each open a real scope, so the body indents itself and closes itself. Block takes a header for anything with no node of its own:

var guarded = method.Lock(Ex.Id("_gate"));        // lock (_gate) { … }
guarded.AddIndentedStatement(Ex.Id("_count").Assign(Ex.Int(1)));

var checkedBlock = method.Block("checked");       // checked { … }
var unsafeBlock  = method.Block("unsafe");        // unsafe  { … }

AddCode is not a substitute — it writes a line and returns without opening a scope, so a hand-written { leaves the body at the wrong indent and the closing brace to you.

Expression bodies, and initializers that do not fit on a line

LambdaSyntax writes => expression; instead of a block, on a method or an accessor. A Return is unwrapped for you, because an expression body takes the expression:

method.LambdaSyntax = true;
method.Return(Ex.Str("x"));                       // public string Describe() => "x";

The array and collection factories write one line, which is right for a few short elements and wrong for a table. The MultiLine forms write one element per line:

field.InitializeValue = Ex.NewArrayMultiLine(rowType, row1, row2, row3);
// = new Row[]
// {
//     row1,
//     row2,
//     row3,
// };

Comments

Comment is a /// documentation comment, and its text is XML — it is escaped for you, so List<string> in a comment does not corrupt the file. For an ordinary // line:

method.AddLineComment("why this is here");        // // why this is here

file.AddAutoGeneratedHeader();                    // // <auto-generated/>, above the usings
file.AddHeaderComment("built by the widget tool");

Every generated file wants AddAutoGeneratedHeader — it is what stops analyzers and style tools reporting on code nobody wrote.

Text templates: AddCode

AddCode is the escape hatch for C# you would rather write as text than build. Prefer Ex — it escapes identifiers, tracks types and handles precedence, none of which a template can do.

An Ex can be substituted into a template like any other value, so the two mix:

method.AddCode("var sum = {arg1};", Ex.Add(Ex.Int(42), Ex.Int(1)));   // var sum = 42 + 1;

AddCode takes a template plus positional values. N is 1-based, and the brackets decide what happens to the value:

Spelling What it does
{argN} Substitutes a value. A type stays a type — qualified per TypeOutputMode, aliased on collision, and its namespace is added to the file's usings.
[argN] Substitutes text, on the spot. Nothing is tracked, so no using is derived.

Measured behaviour, for the same value through each spelling:

Value {argN} [argN]
"hello" hello hello
SyntaxHelpers.QuoteString("hello") "hello" "hello"
42 42 42
42L 42L 42L
1.5d 1.5d 1.5d
1.5m 1.5m 1.5m
true true true
'a' 'a' 'a'
null null null
typeof(StringBuilder) StringBuilder, and using System.Text; is added System.Text.StringBuilder, no using ⚠
DayOfWeek.Monday DayOfWeek.Monday Monday ⚠

The two spellings agree on every scalar. They differ only on a type and on an enum — and on both of those, [argN] is the wrong answer. A type substituted as text is frozen at its identity string: it ignores TypeOutputMode, so it reads the same in a file that qualifies everything, and no using is derived because nothing recorded that a type was mentioned. A bare Monday is CS0103 unless something of that name is in scope, in which case it compiles and means something else.

So: use {argN}. Reach for [argN] only for text that is genuinely text — an identifier, an operator, a fragment of a name you are building up — where the value is a string and the two are equivalent anyway.

A string is code, not a literal

method.AddCode("var s = {arg1};", "hello");                          // var s = hello;
method.AddCode("var s = {arg1};", SyntaxHelpers.QuoteString("hello")); // var s = "hello";

This is the rule everywhere in the library, not just here: a string you hand to CSharpAuthor is a fragment of C#. That is what lets you write AddAttribute(type, $"nameof({member})") or Return("_field.Value") and have it mean what it says. When you want a string literal, ask for one with SyntaxHelpers.QuoteString.

Placeholder mismatches are silent

A placeholder with no value, and a value with no placeholder, are both ignored — the count is never checked, so the mistake reaches your generated file as text:

method.AddCode("var v = {arg0};", "hello");       // var v = {arg0};   0-based is wrong
method.AddCode("var v = {arg2};", "hello");       // var v = {arg2};   no second value
method.AddCode("var v = {arg1} + {arg1};", "a");  // var v = a + a;    every occurrence is filled

Targeting a language version

The tree is version-agnostic; the writer decides how to render it. An EmitProfile says what the target may use, and features downlevel where an equivalent exists — a collection expression becomes new[] { … } below C# 12. Where no equivalent exists, you get a diagnostic rather than wrong output: CSA1001 arrives both as a structured diagnostic and as an inline #error at the declaration that caused it, naming the feature, the version it needs and the version you targeted.

Documentation

This page is the manual — there is no documentation site. Beyond it:

  • Migrating from 1.x
  • Known API gaps — constructs with no first-class entry point. Written against preview1002 and partly stale: the expression and pattern sections have been corrected, the rest has not. Try a construct before believing it is missing.
  • AGENTS.md — conventions, invariants and traps, for humans and AI agents working on the library itself

License

MIT. See LICENSE.

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.
  • .NETStandard 2.0

    • No dependencies.

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
2.0.0 185 9/6/2026
2.0.0-preview1005 1,310 8/28/2026
2.0.0-preview1004 144 8/22/2026
2.0.0-preview1003 103 8/22/2026
2.0.0-preview1002 96 8/21/2026
2.0.0-preview1001 98 8/21/2026
2.0.0-preview1000 93 8/21/2026
1.2.0 291 8/21/2026
1.1.1010 443 8/14/2026
1.1.1009 128 8/13/2026
1.1.1008 108 8/13/2026
1.1.1007 234 8/8/2026
1.1.1006 842 4/27/2025
1.1.1005 243 4/26/2025
1.1.1004 277 4/14/2025
1.1.1003 290 4/13/2025
1.1.1002 185 4/12/2025
1.1.1001 287 3/19/2025
1.0.9 200 3/14/2025
1.0.8 227 3/13/2025
Loading failed