Skip to content

Assembly Scanning

The Switchboard.Extensions.DependencyInjection package provides powerful assembly scanning capabilities to automatically discover and register your flow builders and providers.

Overview

Assembly scanning uses reflection to find classes in your project that follow naming conventions:

  • Classes ending with FlowBuilder are registered as flow builders
  • Classes ending with Provider are registered as providers (for queues, hours, etc.)

How It Works

Automatic Discovery

When you call AddAssemblyScanning() or AddSwitchboardWithScanning(), the framework:

  1. Scans the specified assembly for types
  2. Filters types by naming patterns (*FlowBuilder, *Provider)
  3. Registers matching types as singletons in DI
  4. Makes available for dependency injection and auto-configuration

Naming Conventions

Flow Builders

Any public class ending with FlowBuilder:

csharp
public class SalesInboundFlowBuilder
{
    public IFlow Build()
    {
        return new FlowBuilder()
            .SetName("Sales Inbound")
            .PlayPrompt("Welcome to sales")
            .ThenTransferToQueue("Sales")
            .Build();
    }
}

public class SupportFlowBuilder  // ✅ Discovered
{
    public IFlow Build() { /* ... */ }
}

public class CustomOrderFlowBuilder  // ✅ Discovered
{
    public IFlow Build() { /* ... */ }
}

Providers

Any public class ending with Provider:

csharp
public class QueueConfigurationProvider
{
    public List<IQueue> GetAllQueues()
    {
        return new List<IQueue>
        {
            new QueueBuilder().SetName("Sales").Build(),
            new QueueBuilder().SetName("Support").Build()
        };
    }
}

public class HoursOfOperationProvider  // ✅ Discovered
{
    public HoursOfOperation GetBusinessHours()
    {
        return new HoursOfOperation
        {
            Name = "BusinessHours",
            TimeZone = "America/New_York"
            // Configure hours...
        };
    }
}

public class RoutingProfileProvider  // ✅ Discovered
{
    public List<RoutingProfile> GetProfiles() { /* ... */ }
}

Usage Patterns

Use AddSwitchboardWithScanning() to automatically scan the entry assembly:

csharp
var builder = Host.CreateApplicationBuilder(args);

// Automatically scans Assembly.GetEntryAssembly()
builder.AddSwitchboardWithScanning(options =>
{
    options.InstanceName = "MyCallCenter";
    options.Region = "us-east-1";
});

What gets scanned: The assembly containing your Program.cs


Pattern 2: Manual Assembly Specification

Scan a specific assembly:

csharp
var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddSwitchboard(options =>
{
    options.InstanceName = "MyCallCenter";
    options.Region = "us-east-1";
})
.AddAssemblyScanning(typeof(Program).Assembly);

Pattern 3: Multiple Assemblies

Scan multiple assemblies (useful for large projects):

csharp
builder.Services.AddSwitchboard(options => { })
    .AddAssemblyScanning(typeof(Program).Assembly)           // Main assembly
    .AddAssemblyScanning(typeof(SharedFlows).Assembly)       // Shared flows assembly
    .AddAssemblyScanning(typeof(ExternalProviders).Assembly); // External providers

Pattern 4: Mixed Registration

Combine automatic scanning with manual registration:

csharp
builder.Services.AddSwitchboard(options => { })
    .AddAssemblyScanning(typeof(Program).Assembly)  // Auto-discover most
    .AddFlowBuilders(typeof(SpecialFlowBuilder))    // Manually add specific flow
    .AddProviders(typeof(ExternalQueueProvider));   // Manually add external provider

Discovery Rules

What Gets Discovered

Discovered:

  • Public classes
  • Ending with FlowBuilder or Provider
  • In the scanned assembly
  • Have a parameterless constructor OR resolvable constructor parameters

Not Discovered:

  • Abstract classes
  • Internal/private classes
  • Interfaces
  • Classes without proper naming suffix
  • Generic type definitions

Registration Behavior

All discovered types are registered as Singletons:

csharp
// Equivalent to:
services.AddSingleton<SalesInboundFlowBuilder>();
services.AddSingleton<QueueConfigurationProvider>();

Advanced Scenarios

Dependency Injection in Builders

Flow builders and providers can have constructor dependencies:

csharp
public class SalesInboundFlowBuilder
{
    private readonly ILogger<SalesInboundFlowBuilder> _logger;
    private readonly IConfiguration _configuration;

    public SalesInboundFlowBuilder(
        ILogger<SalesInboundFlowBuilder> logger,
        IConfiguration configuration)
    {
        _logger = logger;
        _configuration = configuration;
    }

    public IFlow Build()
    {
        _logger.LogInformation("Building sales flow");
        var queueName = _configuration["Queues:Sales"];

        return new FlowBuilder()
            .SetName("Sales Inbound")
            .ThenTransferToQueue(queueName)
            .Build();
    }
}

Requirements:

  • All constructor parameters must be registered in DI
  • Framework will resolve dependencies automatically

Organizing Large Projects

Option 1: Feature Folders

MyContactCenter/
├── Features/
│   ├── Sales/
│   │   ├── SalesInboundFlowBuilder.cs
│   │   ├── SalesOutboundFlowBuilder.cs
│   │   └── SalesQueueProvider.cs
│   ├── Support/
│   │   ├── SupportFlowBuilder.cs
│   │   └── SupportQueueProvider.cs
│   └── Shared/
│       ├── HoursOfOperationProvider.cs
│       └── RoutingProfileProvider.cs
└── Program.cs

All discovered automatically with:

csharp
builder.AddSwitchboardWithScanning(options => { });

Option 2: Separate Assemblies

Solution/
├── MyContactCenter.Flows/        # Assembly 1
│   ├── SalesFlowBuilder.cs
│   └── SupportFlowBuilder.cs
├── MyContactCenter.Configuration/ # Assembly 2
│   ├── QueueProvider.cs
│   └── HoursProvider.cs
└── MyContactCenter.Main/          # Assembly 3 (entry)
    └── Program.cs

Scan all assemblies:

csharp
builder.Services.AddSwitchboard(options => { })
    .AddAssemblyScanning(typeof(Program).Assembly)
    .AddAssemblyScanning(typeof(SalesFlowBuilder).Assembly)
    .AddAssemblyScanning(typeof(QueueProvider).Assembly);

Debugging Assembly Scanning

Enable Logging

csharp
var builder = Host.CreateApplicationBuilder(args);

// Enable detailed logging
builder.Logging.SetMinimumLevel(LogLevel.Trace);

builder.AddSwitchboardWithScanning(options =>
{
    options.InstanceName = "MyCallCenter";
});

var host = builder.Build();

// Verify what was registered
var flowBuilders = host.Services.GetServices<object>()
    .Where(s => s.GetType().Name.EndsWith("FlowBuilder"));

foreach (var fb in flowBuilders)
{
    Console.WriteLine($"Discovered: {fb.GetType().Name}");
}

Verify Registration

Check what got registered:

csharp
var host = builder.Build();

// Try resolving a specific builder
var salesFlow = host.Services.GetService<SalesInboundFlowBuilder>();
if (salesFlow != null)
{
    Console.WriteLine("✓ SalesInboundFlowBuilder registered");
}
else
{
    Console.WriteLine("✗ SalesInboundFlowBuilder NOT found");
}

Common Issues

Issue 1: Class Not Discovered

Problem: Your class isn't being found

Checklist:

  • ✅ Class name ends with FlowBuilder or Provider
  • ✅ Class is public (not internal or private)
  • ✅ Class is in the scanned assembly
  • ✅ Class is not abstract
  • ✅ Constructor dependencies are registered in DI

Example Fix:

csharp
// ❌ Won't be discovered (internal)
internal class SalesFlowBuilder { }

// ✅ Will be discovered
public class SalesFlowBuilder { }

Issue 2: Constructor Resolution Fails

Problem: Unable to resolve service for type 'X' while attempting to activate 'YFlowBuilder'

Solution: Register missing dependency:

csharp
builder.Services.AddSingleton<ICustomService, CustomService>();

builder.AddSwitchboardWithScanning(options => { });

Issue 3: Wrong Assembly Scanned

Problem: Classes in different project not found

Solution: Explicitly scan that assembly:

csharp
builder.Services.AddSwitchboard(options => { })
    .AddAssemblyScanning(typeof(ClassInThatAssembly).Assembly);

Performance Considerations

Scanning Cost

Assembly scanning uses reflection and has a one-time startup cost:

  • Small projects (< 50 types): ~1-2ms
  • Large projects (> 500 types): ~10-20ms

This happens once at application startup.

Optimization Tips

  1. Limit scanned assemblies - Only scan assemblies that contain flows/providers
  2. Use manual registration for performance-critical scenarios
  3. Avoid scanning large third-party assemblies
csharp
// ❌ Don't scan everything
.AddAssemblyScanning(AppDomain.CurrentDomain.GetAssemblies())

// ✅ Scan only your assemblies
.AddAssemblyScanning(typeof(Program).Assembly)

Convention Customization

Custom Naming Patterns (Future)

Currently, the framework supports:

  • *FlowBuilder
  • *Provider

Planned: Support for custom patterns via configuration:

csharp
builder.Services.AddSwitchboard(options =>
{
    options.ScanPatterns = new[] { "*FlowBuilder", "*Provider", "*Handler" };
});

Best Practices

1. Consistent Naming

Follow conventions strictly:

csharp
// ✅ Good naming
public class SalesInboundFlowBuilder { }
public class QueueConfigurationProvider { }

// ❌ Bad naming (won't be discovered)
public class SalesFlow { }
public class QueueConfig { }

2. Organize by Feature

Group related builders and providers:

Features/
├── Sales/
│   ├── SalesFlowBuilder.cs
│   └── SalesQueueProvider.cs
└── Support/
    ├── SupportFlowBuilder.cs
    └── SupportQueueProvider.cs

3. Use Dependency Injection

Inject dependencies instead of hardcoding:

csharp
public class SalesFlowBuilder
{
    private readonly IConfiguration _config;

    public SalesFlowBuilder(IConfiguration config)
    {
        _config = config;
    }

    public IFlow Build()
    {
        var queueName = _config["Queues:Sales"];
        // Use config...
    }
}

4. Keep Builders Simple

Builders should focus on flow definition:

csharp
// ✅ Good - simple builder
public class SalesFlowBuilder
{
    public IFlow Build()
    {
        return new FlowBuilder()
            .SetName("Sales")
            .PlayPrompt("Welcome")
            .Build();
    }
}

// ❌ Bad - too much logic
public class SalesFlowBuilder
{
    public IFlow Build()
    {
        // Don't do heavy computation here
        var data = FetchFromDatabase();
        var processed = ComplexProcessing(data);
        // ...
    }
}

Next Steps

Preview release - Licensing terms TBD before 1.0