RFC: Migrate from Stringly-Typed IConfiguration to .NET Options Pattern
Status: Proposed
Created: 2025-10-27
Author: System Analysis
Problem Statement
Currently, Ten Second Tom uses string-based configuration lookups throughout the codebase, which is an anti-pattern in modern .NET applications. We inject IConfiguration and use string keys (even with constants) to retrieve values, losing compile-time safety and violating idiomatic .NET practices.
Current Anti-Pattern Example
public class CreateDailyEntryHandler
{
private readonly IConfiguration _configuration;
public CreateDailyEntryHandler(IConfiguration configuration)
{
_configuration = configuration;
}
public async Task Handle()
{
// ❌ String-based lookup - no compile-time safety
var apiKey = _configuration[ConfigurationKeys.LlmApiKeyKey];
var provider = _configuration[ConfigurationKeys.LlmProviderKey];
// ❌ Manual parsing required
if (!Enum.TryParse<LlmProvider>(provider, out var llmProvider))
{
// Error handling...
}
}
}
Issues:
- ❌ No compile-time safety (typos caught at runtime)
- ❌ No IntelliSense support
- ❌ No validation at startup
- ❌ Scattered parsing logic across codebase
- ❌ Not idiomatic .NET (violates framework conventions)
- ❌ Difficult to test (mocking
IConfiguration is cumbersome)
Current State Analysis
Scope: ~45 string-based configuration lookups across 10 files:
ConfigurationHelpers.cs (2 lookups)
GenerateCommand.cs (2 lookups)
ServiceCollectionExtensions.cs (7 lookups)
ConfigurationChecker.cs (10 lookups)
TemplateMigrationService.cs (1 lookup)
GenerateOutputCommandHandler.cs (1 lookup)
CreateWeeklyReviewHandler.cs (1 lookup)
ConfigCommandHandler.cs (19 lookups)
CreateVoiceNoteEntryHandler.cs (1 lookup)
CreateDailyEntryHandler.cs (1 lookup)
Configuration Sections:
- LLM (Provider, ApiKey, Model, MaxInputTokens)
- SSH (KeyPath, KeySource, AgentSocketPath, KeyDisplayName)
- Audio (STT provider, recording, preprocessing settings)
- Storage (MemoryDirectory, paths)
- Optional (LogLevel, RetentionDays, EnableTelemetry)
Proposed Solution: .NET Options Pattern
The Options Pattern is the idiomatic .NET way to handle strongly-typed configuration.
Example After Refactor
// 1. Define strongly-typed options class
public sealed class LlmOptions
{
public const string SectionName = "TenSecondTom:Llm";
public required LlmProvider Provider { get; init; }
public required string ApiKey { get; init; }
public string? Model { get; init; }
public int MaxInputTokens { get; init; } = 50000;
}
// 2. Register in DI (Program.cs or DependencyInjection.cs)
services.Configure<LlmOptions>(configuration.GetSection(LlmOptions.SectionName));
services.AddSingleton<IValidateOptions<LlmOptions>, LlmOptionsValidator>();
// 3. Inject strongly-typed options
public class CreateDailyEntryHandler
{
private readonly LlmOptions _llmOptions;
public CreateDailyEntryHandler(IOptions<LlmOptions> llmOptions)
{
_llmOptions = llmOptions.Value;
}
public async Task Handle()
{
// ✅ Strongly typed - compile-time safety
var apiKey = _llmOptions.ApiKey;
var provider = _llmOptions.Provider; // Already the enum!
}
}
Implementation Plan
Phase 1: Create Options Classes (Week 1)
Create strongly-typed options classes in src/Shared/Options/:
// LlmOptions.cs
public sealed class LlmOptions
{
public const string SectionName = "TenSecondTom:Llm";
public required LlmProvider Provider { get; init; }
public required string ApiKey { get; init; }
public string? Model { get; init; }
public int MaxInputTokens { get; init; } = 50000;
}
// SshOptions.cs
public sealed class SshOptions
{
public const string SectionName = "TenSecondTom:Ssh";
public string? KeyPath { get; init; }
public SshKeySource? KeySource { get; init; }
public string? AgentSocketPath { get; init; }
public string? KeyDisplayName { get; init; }
}
// AudioOptions.cs (maps to existing AudioConfiguration)
public sealed class AudioOptions
{
public const string SectionName = "TenSecondTom:Audio";
// ... existing AudioConfiguration properties
}
// StorageOptions.cs
public sealed class StorageOptions
{
public const string SectionName = "TenSecondTom:Storage";
public required string MemoryDirectory { get; init; }
public bool CreateIfMissing { get; init; } = true;
}
// AppOptions.cs
public sealed class AppOptions
{
public const string SectionName = "TenSecondTom:Optional";
public LogLevel LogLevel { get; init; } = LogLevel.Information;
public int RetentionDays { get; init; } = 30;
public bool EnableTelemetry { get; init; } = false;
}
Phase 2: Add Validation (Week 1)
Create validators using IValidateOptions<T>:
public sealed class LlmOptionsValidator : IValidateOptions<LlmOptions>
{
public ValidateOptionsResult Validate(string? name, LlmOptions options)
{
if (string.IsNullOrWhiteSpace(options.ApiKey))
return ValidateOptionsResult.Fail("LLM API key is required");
if (!Enum.IsDefined(options.Provider))
return ValidateOptionsResult.Fail($"Invalid LLM provider: {options.Provider}");
if (options.MaxInputTokens <= 0)
return ValidateOptionsResult.Fail("MaxInputTokens must be positive");
return ValidateOptionsResult.Success;
}
}
Phase 3: Register Options in DI (Week 1)
Update ServiceCollectionExtensions.cs:
public static class OptionsRegistration
{
public static IServiceCollection AddTenSecondTomOptions(
this IServiceCollection services,
IConfiguration configuration)
{
// Register options
services.Configure<LlmOptions>(configuration.GetSection(LlmOptions.SectionName));
services.Configure<SshOptions>(configuration.GetSection(SshOptions.SectionName));
services.Configure<AudioOptions>(configuration.GetSection(AudioOptions.SectionName));
services.Configure<StorageOptions>(configuration.GetSection(StorageOptions.SectionName));
services.Configure<AppOptions>(configuration.GetSection(AppOptions.SectionName));
// Register validators
services.AddSingleton<IValidateOptions<LlmOptions>, LlmOptionsValidator>();
services.AddSingleton<IValidateOptions<SshOptions>, SshOptionsValidator>();
services.AddSingleton<IValidateOptions<StorageOptions>, StorageOptionsValidator>();
return services;
}
}
Phase 4: Migrate Services (Weeks 2-3)
Migrate incrementally by feature slice (Vertical Slice Architecture):
Priority Order:
- ✅ Today feature (CreateDailyEntryHandler, CreateVoiceNoteEntryHandler) - 2 files
- ✅ ThisWeek feature (CreateWeeklyReviewHandler) - 1 file
- ✅ Generate feature (GenerateCommand, GenerateOutputCommandHandler) - 2 files
- ✅ Setup feature (ConfigCommandHandler) - 1 file (19 lookups!)
- ✅ Templates feature (TemplateMigrationService) - 1 file
- ✅ Infrastructure (ConfigurationChecker, ServiceCollectionExtensions) - 2 files
- ✅ ConfigurationHelpers - Keep as extension methods for backward compatibility
Per-slice migration steps:
- Update constructor to inject
IOptions<T> or IOptionsSnapshot<T>
- Replace
_configuration[key] with _options.Property
- Remove manual parsing logic
- Update tests to mock
IOptions<T> (easier than IConfiguration)
- Run tests, verify behavior unchanged
Phase 5: Update Tests (Weeks 2-3)
Update unit tests to use Options pattern:
// Before
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
["TenSecondTom:Llm:Provider"] = "OpenAI",
["TenSecondTom:Llm:ApiKey"] = "test-key"
})
.Build();
var handler = new Handler(config);
// After (much cleaner!)
var options = Options.Create(new LlmOptions
{
Provider = LlmProvider.OpenAI,
ApiKey = "test-key"
});
var handler = new Handler(options);
Phase 6: Documentation & Cleanup (Week 3)
- Update
docs/CONFIGURATION.md with Options pattern examples
- Update constitution to mandate Options pattern for new code
- Remove or deprecate
ConfigurationKeys.cs constants (keep only for backward compatibility)
- Add ADR (Architecture Decision Record) documenting the migration
Benefits
Type Safety
- ✅ Compile-time checks for property names and types
- ✅ IntelliSense support in IDE
- ✅ Refactoring tools work correctly
Validation
- ✅ Configuration validated at startup (fail-fast)
- ✅ Clear error messages for misconfigurations
- ✅ Prevents runtime errors from invalid config
Testability
- ✅ Easy to mock
IOptions<T> in tests
- ✅ No need to build complex
IConfiguration objects
- ✅ Tests are more readable
Maintainability
- ✅ Configuration schema documented in code (single source of truth)
- ✅ No scattered parsing logic
- ✅ Easier to refactor configuration structure
Idiomatic .NET
- ✅ Follows Microsoft best practices
- ✅ Aligns with ASP.NET Core conventions
- ✅ Consistent with modern .NET applications
Risks & Considerations
Breaking Changes
- Mitigation: Keep
ConfigurationHelpers extension methods for backward compatibility during transition
- Strategy: Migrate incrementally, one feature at a time
Test Updates Required
- Impact: ~45 test files may need updates
- Mitigation: Update tests as we migrate each feature slice
Learning Curve
- Impact: Team needs to understand Options pattern
- Mitigation: Provide examples and documentation, start with simple features
Configuration File Compatibility
- Impact: None - JSON structure remains unchanged
- Note: Options pattern reads the same configuration keys
Success Criteria
Timeline
- Week 1: Create Options classes, validators, DI registration
- Week 2-3: Migrate services incrementally (one slice at a time)
- Week 3: Update tests, documentation, cleanup
Total Estimated Effort: 2-3 weeks (with test-first approach)
Related Documentation
Decision
Status: PROPOSED - Awaiting approval
Recommended Action: Approve and schedule for implementation in next sprint
Labels: refactor, tech-debt, idiomatic-csharp
Priority: Medium (improves quality, not blocking features)
Effort: 2-3 weeks
RFC: Migrate from Stringly-Typed IConfiguration to .NET Options Pattern
Status: Proposed
Created: 2025-10-27
Author: System Analysis
Problem Statement
Currently, Ten Second Tom uses string-based configuration lookups throughout the codebase, which is an anti-pattern in modern .NET applications. We inject
IConfigurationand use string keys (even with constants) to retrieve values, losing compile-time safety and violating idiomatic .NET practices.Current Anti-Pattern Example
Issues:
IConfigurationis cumbersome)Current State Analysis
Scope: ~45 string-based configuration lookups across 10 files:
ConfigurationHelpers.cs(2 lookups)GenerateCommand.cs(2 lookups)ServiceCollectionExtensions.cs(7 lookups)ConfigurationChecker.cs(10 lookups)TemplateMigrationService.cs(1 lookup)GenerateOutputCommandHandler.cs(1 lookup)CreateWeeklyReviewHandler.cs(1 lookup)ConfigCommandHandler.cs(19 lookups)CreateVoiceNoteEntryHandler.cs(1 lookup)CreateDailyEntryHandler.cs(1 lookup)Configuration Sections:
Proposed Solution: .NET Options Pattern
The Options Pattern is the idiomatic .NET way to handle strongly-typed configuration.
Example After Refactor
Implementation Plan
Phase 1: Create Options Classes (Week 1)
Create strongly-typed options classes in
src/Shared/Options/:Phase 2: Add Validation (Week 1)
Create validators using
IValidateOptions<T>:Phase 3: Register Options in DI (Week 1)
Update
ServiceCollectionExtensions.cs:Phase 4: Migrate Services (Weeks 2-3)
Migrate incrementally by feature slice (Vertical Slice Architecture):
Priority Order:
Per-slice migration steps:
IOptions<T>orIOptionsSnapshot<T>_configuration[key]with_options.PropertyIOptions<T>(easier thanIConfiguration)Phase 5: Update Tests (Weeks 2-3)
Update unit tests to use Options pattern:
Phase 6: Documentation & Cleanup (Week 3)
docs/CONFIGURATION.mdwith Options pattern examplesConfigurationKeys.csconstants (keep only for backward compatibility)Benefits
Type Safety
Validation
Testability
IOptions<T>in testsIConfigurationobjectsMaintainability
Idiomatic .NET
Risks & Considerations
Breaking Changes
ConfigurationHelpersextension methods for backward compatibility during transitionTest Updates Required
Learning Curve
Configuration File Compatibility
Success Criteria
Timeline
Total Estimated Effort: 2-3 weeks (with test-first approach)
Related Documentation
Decision
Status: PROPOSED - Awaiting approval
Recommended Action: Approve and schedule for implementation in next sprint
Labels:
refactor,tech-debt,idiomatic-csharpPriority: Medium (improves quality, not blocking features)
Effort: 2-3 weeks