Skip to content

Source Generators Guide ​

PREVIEW RELEASE

Switchboard is currently in preview (v0.1.0-preview.62). 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 ​

bash
dotnet add package NickSoftware.Switchboard.SourceGenerators --version 0.1.0-preview.62

The 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 ​

  1. You Write - Declarative code with attributes
  2. Generator Analyzes - At compile time, generator scans your code
  3. Code Generated - Implementation created automatically
  4. You Use - Generated code available immediately in IDE

Example Flow ​

Your Code (Input):

csharp
[ContactFlow("SalesFlow")]
public partial class SalesFlow : FlowDefinitionBase
{
    [Action(Order = 1)]
    [Message("Welcome to sales")]
    public partial void Welcome();
}

Generated Code (Output):

csharp
// 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:

csharp
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:

csharp
// <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:

csharp
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:

csharp
// <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:

csharp
using Switchboard.Lambda;

public interface IConfigFetcher
{
    [LambdaHandler]
    Task<FlowConfig> GetFlowConfigAsync(string flowId, string version);

    [LambdaHandler]
    Task<QueueConfig> GetQueueConfigAsync(string queueId);
}

Generated Output:

csharp
// <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

csharp
// 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

bash
dotnet build

The source generator runs automatically during build.

Step 3: View Generated Code

In Visual Studio or Rider:

  1. Expand the project node
  2. Find "Dependencies" → "Analyzers" → "Switchboard.SourceGenerators"
  3. See the generated WelcomeFlow.g.cs

Step 4: Use the Flow

csharp
// Program.cs
var flow = new WelcomeFlow();
await flow.ExecuteAsync(); // Generated methods work!

Example 2: Complex Flow with Branching ​

Step 1: Define the Flow

csharp
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() implementation
  • GetInput() implementation with input collection
  • RouteCall() implementation with condition evaluation
  • TransferToTech(), TransferToBilling(), HandleInvalid() implementations
  • Transition management between actions
  • State tracking

Step 3: Use It

csharp
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

csharp
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

csharp
var stack = new ConnectStack(app, "MyStack");
stack.AddFlow(new CustomerLookupFlow());
app.Synth();

Viewing Generated Code ​

Visual Studio ​

  1. Solution Explorer → Project → Dependencies
  2. Expand "Analyzers"
  3. Expand "Switchboard.SourceGenerators"
  4. View .g.cs files

Rider ​

  1. Project View → Project
  2. Show "Source Generators" folder
  3. Expand to see generated files

Command Line ​

bash
# Generated files are in obj/
find ./obj -name "*.g.cs"

Debugging Generated Code ​

Enable Source Generator Logging ​

Add to your .csproj:

xml
<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
  <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)/Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

Then rebuild:

bash
dotnet build

Generated files will be in obj/Generated/.

Common Issues ​

Issue: Generated code not appearing

Solutions:

  1. Clean and rebuild:

    bash
    dotnet clean
    dotnet build
  2. Check package reference:

    xml
    <PackageReference Include="NickSoftware.Switchboard.SourceGenerators"
                      Version="0.1.0-preview.62" />
  3. Ensure class is partial:

    csharp
    public 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:

csharp
public partial class MyFlow : FlowDefinitionBase { }

Won't Work:

csharp
public class MyFlow : FlowDefinitionBase { } // Missing partial!

2. Use Partial Methods ​

Required:

csharp
public partial void MyAction();

Won't Work:

csharp
public void MyAction() { } // Can't have body in declaration

3. Order Actions Logically ​

csharp
[Action(Order = 1)]  // Start
[Action(Order = 2)]  // Middle
[Action(Order = 3)]  // End

4. Use Descriptive Identifiers ​

Good:

csharp
[Action(Order = 4, Identifier = "TransferToVIPSupport")]

Bad:

csharp
[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:

csharp
[FlowAction("CustomMessage")]
public class CustomMessageAction : FlowAction
{
    public string Text { get; set; }
    public string VoiceId { get; set; }
    public int RepeatCount { get; set; }
}

Use in flows:

csharp
[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:

  1. Package installed correctly
  2. Class marked as partial
  3. Using supported .NET version (10.0+)
  4. Build output for generator errors

Unexpected Generated Code ​

Debug:

  1. Enable EmitCompilerGeneratedFiles
  2. Review generated .g.cs files
  3. Check attribute usage
  4. Verify attribute properties are correct

Conflicts with Manual Code ​

Solution: Don't mix manual and generated implementations:

Bad:

csharp
// Your code
public partial void MyAction()
{
    // Manual implementation
}

// Also decorated with attributes - conflict!
[Action(Order = 1)]
[Message("Test")]
public partial void MyAction();

Good:

csharp
// Use either generated OR manual, not both
[Action(Order = 1)]
[Message("Test")]
public partial void MyAction(); // Generator creates implementation

See Also ​

Preview release - Licensing terms TBD before 1.0