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
FlowBuilderare registered as flow builders - Classes ending with
Providerare registered as providers (for queues, hours, etc.)
How It Works
Automatic Discovery
When you call AddAssemblyScanning() or AddSwitchboardWithScanning(), the framework:
- Scans the specified assembly for types
- Filters types by naming patterns (
*FlowBuilder,*Provider) - Registers matching types as singletons in DI
- Makes available for dependency injection and auto-configuration
Naming Conventions
Flow Builders
Any public class ending with FlowBuilder:
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:
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
Pattern 1: Automatic Scanning (Recommended)
Use AddSwitchboardWithScanning() to automatically scan the entry assembly:
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:
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):
builder.Services.AddSwitchboard(options => { })
.AddAssemblyScanning(typeof(Program).Assembly) // Main assembly
.AddAssemblyScanning(typeof(SharedFlows).Assembly) // Shared flows assembly
.AddAssemblyScanning(typeof(ExternalProviders).Assembly); // External providersPattern 4: Mixed Registration
Combine automatic scanning with manual registration:
builder.Services.AddSwitchboard(options => { })
.AddAssemblyScanning(typeof(Program).Assembly) // Auto-discover most
.AddFlowBuilders(typeof(SpecialFlowBuilder)) // Manually add specific flow
.AddProviders(typeof(ExternalQueueProvider)); // Manually add external providerDiscovery Rules
What Gets Discovered
✅ Discovered:
- Public classes
- Ending with
FlowBuilderorProvider - 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:
// Equivalent to:
services.AddSingleton<SalesInboundFlowBuilder>();
services.AddSingleton<QueueConfigurationProvider>();Advanced Scenarios
Dependency Injection in Builders
Flow builders and providers can have constructor dependencies:
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.csAll discovered automatically with:
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.csScan all assemblies:
builder.Services.AddSwitchboard(options => { })
.AddAssemblyScanning(typeof(Program).Assembly)
.AddAssemblyScanning(typeof(SalesFlowBuilder).Assembly)
.AddAssemblyScanning(typeof(QueueProvider).Assembly);Debugging Assembly Scanning
Enable Logging
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:
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
FlowBuilderorProvider - ✅ Class is
public(notinternalorprivate) - ✅ Class is in the scanned assembly
- ✅ Class is not abstract
- ✅ Constructor dependencies are registered in DI
Example Fix:
// ❌ 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:
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:
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
- Limit scanned assemblies - Only scan assemblies that contain flows/providers
- Use manual registration for performance-critical scenarios
- Avoid scanning large third-party assemblies
// ❌ 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:
builder.Services.AddSwitchboard(options =>
{
options.ScanPatterns = new[] { "*FlowBuilder", "*Provider", "*Handler" };
});Best Practices
1. Consistent Naming
Follow conventions strictly:
// ✅ 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.cs3. Use Dependency Injection
Inject dependencies instead of hardcoding:
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:
// ✅ 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);
// ...
}
}