Source Generators Guide
PREVIEW RELEASE
Switchboard is currently in preview (v0.1.0-preview.17). Source generators are actively being developed.
Source generators in Switchboard automatically generate boilerplate code at compile-time, reducing repetitive code and ensuring type safety.
Table of Contents
Overview
What Are Source Generators?
Source generators are compile-time code generators that:
- Run during compilation (before your code builds)
- Analyze your attributes and models
- Generate additional C# code automatically
- Provide full IntelliSense support for generated code
- Catch errors at build time
Benefits
✅ Less Boilerplate - Write attributes, get implementation for free ✅ Type Safety - Generated code is strongly typed ✅ IntelliSense - Full IDE support for generated methods ✅ Compile-Time Validation - Errors caught before runtime ✅ Zero Runtime Cost - Code generated at build time, not runtime
Installation
dotnet add package NickSoftware.Switchboard.SourceGenerators --version 0.1.0-preview.17The source generators package is automatically referenced when you install the main Switchboard package, but you can add it explicitly for more control.
How Source Generators Work
The Process
- You Write - Declarative code with attributes
- Generator Analyzes - At compile time, generator scans your code
- Code Generated - Implementation created automatically
- You Use - Generated code available immediately in IDE
Example Flow
Your Code (Input):
[ContactFlow("SalesFlow")]
public partial class SalesFlow : FlowDefinitionBase
{
[Action(Order = 1)]
[Message("Welcome to sales")]
public partial void Welcome();
}Generated Code (Output):
// Auto-generated by Switchboard Source Generator
public partial class SalesFlow
{
partial void Welcome()
{
var action = new MessageAction
{
Identifier = "Welcome",
Text = "Welcome to sales"
};
this.AddAction(action);
}
}Available Source Generators
1. Flow Definition Generator
Generates flow implementation from attribute-decorated partial methods.
What It Generates:
- Flow action implementations
- State management code
- Transition logic
- Validation code
Example:
Your Input:
using Switchboard.Attributes;
namespace MyApp.Flows;
[ContactFlow("CustomerService")]
public partial class CustomerServiceFlow : FlowDefinitionBase
{
[Action(Order = 1)]
[Message("Welcome to customer service")]
public partial void Welcome();
[Action(Order = 2)]
[GetCustomerInput]
[Text("Press 1 for sales, 2 for support")]
[MaxDigits(1)]
[Timeout(5)]
public partial Task<string> GetSelection();
[Action(Order = 3)]
[TransferToQueue("Support")]
public partial void TransferToSupport();
}Generated Output:
// <auto-generated/>
namespace MyApp.Flows
{
partial class CustomerServiceFlow
{
partial void Welcome()
{
var action = new MessageAction
{
Identifier = "Welcome",
Text = "Welcome to customer service",
Order = 1
};
AddAction(action);
}
partial async Task<string> GetSelection()
{
var action = new GetCustomerInputAction
{
Identifier = "GetSelection",
Text = "Press 1 for sales, 2 for support",
MaxDigits = 1,
TimeoutSeconds = 5,
Order = 2
};
AddAction(action);
return await ExecuteAndGetResultAsync<string>(action);
}
partial void TransferToSupport()
{
var action = new TransferToQueueAction
{
Identifier = "TransferToSupport",
QueueName = "Support",
Order = 3
};
AddAction(action);
}
}
}2. DynamoDB Schema Generator
Generates DynamoDB table definitions from model classes.
What It Generates:
- Table schema definitions
- Attribute mappings
- Index definitions
- Migration helpers
Example:
Your Input:
using Switchboard.Configuration;
[DynamoDbTable("FlowConfigurations")]
public class FlowConfiguration
{
[PartitionKey]
public string FlowId { get; set; }
[SortKey]
public string Version { get; set; }
[Attribute]
public string Name { get; set; }
[Attribute]
public Dictionary<string, string> Parameters { get; set; }
[GlobalSecondaryIndex("ByName")]
public string SearchableName { get; set; }
}Generated Output:
// <auto-generated/>
public static class FlowConfigurationTableDefinition
{
public static Table CreateTable(Construct scope, string id)
{
return new Table(scope, id, new TableProps
{
TableName = "FlowConfigurations",
PartitionKey = new Attribute
{
Name = "FlowId",
Type = AttributeType.STRING
},
SortKey = new Attribute
{
Name = "Version",
Type = AttributeType.STRING
},
BillingMode = BillingMode.PAY_PER_REQUEST,
GlobalSecondaryIndexes = new[]
{
new GlobalSecondaryIndexProps
{
IndexName = "ByName",
PartitionKey = new Attribute
{
Name = "SearchableName",
Type = AttributeType.STRING
}
}
}
});
}
}3. Lambda Handler Generator
Generates AWS Lambda handlers from interfaces.
What It Generates:
- Lambda function handler methods
- Input/output serialization
- Error handling boilerplate
- CloudWatch logging setup
Example:
Your Input:
using Switchboard.Lambda;
public interface IConfigFetcher
{
[LambdaHandler]
Task<FlowConfig> GetFlowConfigAsync(string flowId, string version);
[LambdaHandler]
Task<QueueConfig> GetQueueConfigAsync(string queueId);
}Generated Output:
// <auto-generated/>
public class ConfigFetcherLambdaHandler
{
private readonly IConfigFetcher _service;
public ConfigFetcherLambdaHandler(IConfigFetcher service)
{
_service = service;
}
[LambdaSerializer(typeof(DefaultLambdaJsonSerializer))]
public async Task<FlowConfig> GetFlowConfigHandler(
GetFlowConfigRequest request,
ILambdaContext context)
{
context.Logger.LogLine($"Fetching config for flow: {request.FlowId}");
try
{
return await _service.GetFlowConfigAsync(
request.FlowId,
request.Version);
}
catch (Exception ex)
{
context.Logger.LogLine($"Error: {ex.Message}");
throw;
}
}
[LambdaSerializer(typeof(DefaultLambdaJsonSerializer))]
public async Task<QueueConfig> GetQueueConfigHandler(
GetQueueConfigRequest request,
ILambdaContext context)
{
context.Logger.LogLine($"Fetching config for queue: {request.QueueId}");
try
{
return await _service.GetQueueConfigAsync(request.QueueId);
}
catch (Exception ex)
{
context.Logger.LogLine($"Error: {ex.Message}");
throw;
}
}
}Step-by-Step Tutorial
Example 1: Creating Your First Generated Flow
Step 1: Create the Flow Definition
// Flows/WelcomeFlow.cs
using Switchboard.Attributes;
namespace MyCallCenter.Flows;
[ContactFlow("WelcomeFlow")]
public partial class WelcomeFlow : FlowDefinitionBase
{
// Partial methods - implementations will be generated
[Action(Order = 1)]
[Message("Thank you for calling")]
public partial void Greeting();
[Action(Order = 2)]
[TransferToQueue("Sales")]
public partial void Transfer();
}Step 2: Build the Project
dotnet buildThe source generator runs automatically during build.
Step 3: View Generated Code
In Visual Studio or Rider:
- Expand the project node
- Find "Dependencies" → "Analyzers" → "Switchboard.SourceGenerators"
- See the generated
WelcomeFlow.g.cs
Step 4: Use the Flow
// Program.cs
var flow = new WelcomeFlow();
await flow.ExecuteAsync(); // Generated methods work!Example 2: Complex Flow with Branching
Step 1: Define the Flow
using Switchboard.Attributes;
namespace MyCallCenter.Flows;
[ContactFlow("SupportRouter")]
public partial class SupportRouterFlow : FlowDefinitionBase
{
[Action(Order = 1)]
[Message("Welcome to support")]
public partial void Welcome();
[Action(Order = 2)]
[GetCustomerInput]
[Text("For technical support, press 1. For billing, press 2.")]
[MaxDigits(1)]
[Timeout(5)]
public partial Task<string> GetInput();
[Action(Order = 3)]
[Branch(AttributeName = "CustomerInput")]
[Case("1", Target = "TechnicalSupport")]
[Case("2", Target = "BillingSupport")]
[DefaultCase(Target = "InvalidInput")]
public partial void RouteCall();
[Action(Order = 4, Identifier = "TechnicalSupport")]
[TransferToQueue("TechSupport")]
public partial void TransferToTech();
[Action(Order = 5, Identifier = "BillingSupport")]
[TransferToQueue("Billing")]
public partial void TransferToBilling();
[Action(Order = 6, Identifier = "InvalidInput")]
[Message("Invalid selection")]
[Transition(Target = "GetInput")]
public partial void HandleInvalid();
}Step 2: The Generator Creates:
Welcome()implementationGetInput()implementation with input collectionRouteCall()implementation with condition evaluationTransferToTech(),TransferToBilling(),HandleInvalid()implementations- Transition management between actions
- State tracking
Step 3: Use It
var router = new SupportRouterFlow();
await router.ExecuteAsync();The generated code handles all the complexity!
Example 3: Customer Lookup with Lambda Integration
Step 1: Define the Flow
using Switchboard.Attributes;
namespace MyCallCenter.Flows;
[ContactFlow("CustomerLookup")]
public partial class CustomerLookupFlow : FlowDefinitionBase
{
[Action(Order = 1)]
[GetCustomerInput]
[Text("Please enter your account number")]
[MaxDigits(10)]
public partial Task<string> GetAccountNumber();
[Action(Order = 2)]
[InvokeLambda]
[FunctionName("CustomerLookupFunction")]
[InputParameter("accountNumber", "$.Attributes.CustomerInput")]
public partial Task<LambdaResult> LookupCustomer();
[Action(Order = 3)]
[SetContactAttributes]
[Attribute("CustomerName", "$.Lambda.Name")]
[Attribute("CustomerTier", "$.Lambda.Tier")]
[Attribute("AccountBalance", "$.Lambda.Balance")]
public partial void SetCustomerData();
[Action(Order = 4)]
[Branch(AttributeName = "CustomerTier")]
[Case("VIP", Target = "VIPQueue")]
[DefaultCase(Target = "StandardQueue")]
public partial void RouteByTier();
[Action(Order = 5, Identifier = "VIPQueue")]
[Message("Welcome, valued customer!")]
[TransferToQueue("VIPSupport")]
public partial void RouteToVIP();
[Action(Order = 6, Identifier = "StandardQueue")]
[TransferToQueue("StandardSupport")]
public partial void RouteToStandard();
}Step 2: Generated Code Handles:
- Account number collection
- Lambda invocation with proper parameter mapping
- Contact attribute setting from Lambda results
- Conditional routing based on customer tier
- Queue transfers
Step 3: Deploy and Use
var stack = new ConnectStack(app, "MyStack");
stack.AddFlow(new CustomerLookupFlow());
app.Synth();Viewing Generated Code
Visual Studio
- Solution Explorer → Project → Dependencies
- Expand "Analyzers"
- Expand "Switchboard.SourceGenerators"
- View
.g.csfiles
Rider
- Project View → Project
- Show "Source Generators" folder
- Expand to see generated files
Command Line
# Generated files are in obj/
find ./obj -name "*.g.cs"Debugging Generated Code
Enable Source Generator Logging
Add to your .csproj:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)/Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>Then rebuild:
dotnet buildGenerated files will be in obj/Generated/.
Common Issues
Issue: Generated code not appearing
Solutions:
Clean and rebuild:
bashdotnet clean dotnet buildCheck package reference:
xml<PackageReference Include="NickSoftware.Switchboard.SourceGenerators" Version="0.1.0-preview.17" />Ensure class is
partial:csharppublic partial class MyFlow // Must be partial!
Issue: Build errors in generated code
Solution: Check your attribute usage - the analyzer will help catch errors.
Best Practices
1. Always Use Partial Classes
Required:
public partial class MyFlow : FlowDefinitionBase { }Won't Work:
public class MyFlow : FlowDefinitionBase { } // Missing partial!2. Use Partial Methods
Required:
public partial void MyAction();Won't Work:
public void MyAction() { } // Can't have body in declaration3. Order Actions Logically
[Action(Order = 1)] // Start
[Action(Order = 2)] // Middle
[Action(Order = 3)] // End4. Use Descriptive Identifiers
Good:
[Action(Order = 4, Identifier = "TransferToVIPSupport")]Bad:
[Action(Order = 4, Identifier = "Action4")]5. Leverage IntelliSense
The generator creates code you can immediately use with full IDE support.
Performance
Compile-Time Impact
Source generators add minimal compile time:
- Small project: < 1 second
- Large project: 1-3 seconds
Runtime Impact
Zero runtime cost! Code is generated once at build time.
Advanced Scenarios
Custom Action Types
Define your own action types:
[FlowAction("CustomMessage")]
public class CustomMessageAction : FlowAction
{
public string Text { get; set; }
public string VoiceId { get; set; }
public int RepeatCount { get; set; }
}Use in flows:
[Action(Order = 1)]
[CustomMessage(Text = "Hello", VoiceId = "Joanna", RepeatCount = 2)]
public partial void Greet();The generator will create the implementation!
Troubleshooting
Generator Not Running
Check:
- Package installed correctly
- Class marked as
partial - Using supported .NET version (10.0+)
- Build output for generator errors
Unexpected Generated Code
Debug:
- Enable
EmitCompilerGeneratedFiles - Review generated
.g.csfiles - Check attribute usage
- Verify attribute properties are correct
Conflicts with Manual Code
Solution: Don't mix manual and generated implementations:
Bad:
// Your code
public partial void MyAction()
{
// Manual implementation
}
// Also decorated with attributes - conflict!
[Action(Order = 1)]
[Message("Test")]
public partial void MyAction();Good:
// Use either generated OR manual, not both
[Action(Order = 1)]
[Message("Test")]
public partial void MyAction(); // Generator creates implementationSee Also
- Attributes Reference - All available attributes
- Roslyn Analyzers - Compile-time validation
- Examples - Working examples
- Flow Building Guide - Complete flow construction