Skip to content

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

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

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.17" />
  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