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()"));