Skip to content

Emit

Fluent builders for generating compilable C# code.

See also: Queries | Projections | Extensions

Overview

Emit builders construct Roslyn syntax trees using a fluent, immutable API. Where queries find code, emit builders create it.

Builder Description
TypeBuilder Create classes, interfaces, structs, records
TypeBuilder Extensions Interface and operator implementations
MethodBuilder Create methods
PropertyBuilder Create properties
FieldBuilder Create fields
ConstructorBuilder Create constructors
OperatorBuilder Create operator overloads
ConversionOperatorBuilder Create explicit/implicit conversion operators
EventBuilder Create event declarations
IndexerBuilder Create indexer declarations
ParameterBuilder Create parameter declarations
TypeParameterBuilder Create generic type parameter declarations
BodyBuilder Create method/property bodies
CatchBuilder Compose catch clauses for try-catch
CommentBuilder Build single-line and block comments
SwitchStatementBuilder Compose switch statement sections
SwitchExpressionBuilder Compose switch expression arms
EmitWriter C#-aware text writer for high-performance source output
Patterns Builder, Singleton, ToString extensions
Directives Preprocessor directives for conditional compilation
GlobalUsings Emit global using directives as a complete source file
Support Types AttributeBuilder, XmlDocumentationBuilder, EmitOptions, IWritable

Builders use the Type System primitives (TypeRef, ExpressionRef, etc.) and typed wrappers for type-safe code generation.

All builders are immutable — each method returns a new instance.

var result = TypeBuilder
    .Class("Customer")
    .InNamespace("MyApp.Domain")
    .AsPartial()
    .AddProperty("Name", "string", p => p
        .WithAccessibility(Accessibility.Public)
        .WithAutoPropertyAccessors())
    .Emit();

if (result.IsValid(out var valid))
{
    string code = valid.Code;                    // Formatted C# code
    CompilationUnitSyntax syntax = valid.Syntax; // Roslyn syntax tree
}

Parse API

Build from natural C# signatures — parse the signature, then customize with builder methods:

// Parse a method signature, then add the body
var method = MethodBuilder.Parse("public async Task<bool> ProcessAsync(string input, CancellationToken ct = default)")
    .WithBody(b => b
        .AddStatement("await Task.Delay(100, ct)")
        .AddReturn("true"));

// Parse a property, optionally customize further
var property = PropertyBuilder.Parse("public string Name { get; set; }")
    .WithAttribute("Required");

// Parse a field
var field = FieldBuilder.Parse("private readonly ILogger _logger");

// Parse a type signature with base types, then add members
var service = TypeBuilder.Parse("public sealed class CustomerService : ICustomerService")
    .InNamespace("MyApp.Services")
    .AddField(field)
    .AddProperty(property)
    .AddMethod(method);

What Parse Handles

Parsed from Signature Added via Builder Methods
Name, type, return type Method/property bodies
Accessibility (public, private, etc.) Attributes
Modifiers (static, async, virtual, readonly, etc.) XML documentation
Parameters with modifiers and defaults Namespace and usings
Generic type parameters and constraints Additional members
Base types and interfaces
Property accessors ({ get; set; })
Field initializers

Parse Examples

// Methods with generics and constraints
MethodBuilder.Parse("public T Convert<T>(object value) where T : class")

// Abstract and virtual methods
MethodBuilder.Parse("protected virtual void OnPropertyChanged(string name)")

// Properties with various accessors
PropertyBuilder.Parse("public int Count { get; }")
PropertyBuilder.Parse("public List<string> Items { get; set; } = new()")

// Fields with modifiers
FieldBuilder.Parse("private static int _count = 0")
FieldBuilder.Parse("public const int MaxRetries = 3")

// Types with inheritance
TypeBuilder.Parse("public abstract class BaseHandler<T> : IHandler<T> where T : class")
TypeBuilder.Parse("public partial record OrderDto(string Id, decimal Total)")

OptionalEmit

The result of emitting code.

var result = builder.Emit();

// Check if valid
if (result.IsValid(out var valid))
{
    string code = valid.Code;                    // Formatted C# code
    CompilationUnitSyntax syntax = valid.Syntax; // Roslyn syntax tree
}

// Check if invalid
if (result.IsNotValid(out var diagnostics))
{
    foreach (var diagnostic in diagnostics)
    {
        Console.WriteLine(diagnostic.GetMessage());
    }
}

// Get diagnostics (warnings even if valid)
ImmutableArray<Diagnostic> diags = result.Diagnostics;

ValidEmit Part Extraction

Once validated, ValidEmit exposes structured access to the generated compilation unit:

valid.Usings          // ImmutableArray<UsingDirectiveSyntax>
valid.Types           // ImmutableArray<MemberDeclarationSyntax> (unwrapped from namespaces)
valid.Namespace       // string? — the namespace name, or null for global scope
valid.LeadingTrivia   // SyntaxTriviaList — header comments, nullable directives, etc.

Combining Emit Results

Use Combine() to merge multiple emit results into a single compilation unit. Usings are deduplicated and types are grouped by namespace.

// Combine two ValidEmit results
var combined = emitA.Combine(emitB);

// Chain multiple
var all = emitA.Combine(emitB).Combine(emitC);

// Works with OptionalEmit too — propagates failures and aggregates diagnostics
OptionalEmit merged = optionalA.Combine(optionalB);

Complete Example

var result = TypeBuilder
    .Class("CustomerRepository")
    .InNamespace("MyApp.Data")
    .AddUsing("System")
    .AddUsing("System.Threading.Tasks")
    .Implements("ICustomerRepository")
    .WithXmlDoc("Repository for customer data access.")
    .AddField("_context", "DbContext", f => f
        .WithAccessibility(Accessibility.Private)
        .AsReadonly())
    .AddConstructor(ctor => ctor
        .WithAccessibility(Accessibility.Public)
        .AddParameter("context", "DbContext")
        .WithBody(body => body.AddStatement("_context = context;")))
    .AddMethod("GetByIdAsync", m => m
        .WithReturnType("Task<Customer?>")
        .WithAccessibility(Accessibility.Public)
        .Async()
        .AddParameter("id", "Guid")
        .AddParameter("cancellationToken", "CancellationToken", p => p.WithDefaultValue("default"))
        .WithXmlDoc(doc => doc
            .Summary("Gets a customer by identifier.")
            .Param("id", "The customer identifier.")
            .Param("cancellationToken", "Cancellation token.")
            .Returns("The customer if found; otherwise, null."))
        .WithBody(body => body
            .AddReturn("await _context.Customers.FindAsync(new object[] { id }, cancellationToken)")))
    .Emit();

if (result.IsValid(out var valid))
{
    context.AddSource("CustomerRepository.g.cs", valid.Code);
}

Conditional Compilation

Use Directives to generate framework-specific code:

TypeBuilder.Struct("UserId")
    .Implements("IEquatable<UserId>")
    .Implements("ISpanFormattable", Directives.Net6OrGreater)
    .Implements("IParsable<UserId>", Directives.Net7OrGreater)
    .AddMethod("TryFormat", m => m
        .When(Directives.Net6OrGreater)
        .WithReturnType("bool")
        .WithBody(...));

Real-World Usage

Generating from Analyzed Symbols

// Generate a module class from analyzed type information
return TypeBuilder.Parse($"public static partial class {model.EffectsContainerName}")
    .AddUsings(usings)
    .InNamespace(model.Namespace)
    .AddNestedType(module)
    .Emit(options ?? EmitOptions.Default);

Adding Methods Dynamically

var builder = TypeBuilder.Class("Generated");

foreach (var method in methods)
{
    builder = builder.AddMethod(method.Name, m => m
        .AsStatic()
        .WithReturnType($"Eff<RT, {method.ReturnType}>")
        .AddParameter("input", method.InputType)
        .WithXmlDoc(method.XmlDocumentation)
        .WithExpressionBody(GenerateBody(method)));
}

Using Parse for Complex Signatures

// Parse handles complex signatures more naturally
var method = MethodBuilder.Parse("public async Task<IEnumerable<Customer>> GetAllAsync()")
    .AddParameter("filter", "CustomerFilter", p => p.WithDefaultValue("null"))
    .WithBody(body => body
        .AddStatement("var query = _context.Customers.AsQueryable();")
        .AddStatement("if (filter != null) query = query.Where(filter.ToPredicate());")
        .AddReturn("await query.ToListAsync()"));