Strategy Pattern In Modules Domain
Purpose
This document explains the Strategy pattern and how it is applied in the Modules Domain.
What Is The Strategy Pattern?
The Strategy pattern defines a family of interchangeable algorithms behind a common interface. A caller chooses one strategy at runtime and executes it through that shared contract.
In practical terms:
- The context depends on an interface, not concrete implementations.
- New strategies can be added without changing existing calling code.
- Runtime selection decides which behavior is executed.
Strategy Pattern Mapping In Imparta.iCoachAi.Api
Strategy Interface
IModuledefines the common operation:Execute().
Concrete Strategies
Current strategy implementations in the Domain folder:
AzureSpeechToTextAzureTextToSpeechEmail
Each class implements IModule and provides its own Execute() behavior.
Context / Selector
ModuleProvideris the context/factory that selects a concrete strategy by name.- It discovers module implementations from the assembly and stores them in a frozen lookup dictionary.
- It creates the selected strategy instance using DI (
ActivatorUtilities.CreateInstance).
Runtime Entry Points
GET /strategiesreturns available strategy names.POST /strategies/{name}selects the strategy by name and executesExecute().
Structural Diagram
classDiagram
class IModule {
<<interface>>
+Execute() void
}
class AzureSpeechToText {
+Execute() void
}
class AzureTextToSpeech {
+Execute() void
}
class Email {
+Execute() void
}
class ModuleProvider {
-FrozenModuleTypes: FrozenDictionary~string, Type~
+AvailableModules: IEnumerable~string~
+Create(moduleName: string) IModule
}
class ModuleEndpoints {
+GetAvailableStrategies()
+ExecuteStrategy(name)
}
IModule <|.. AzureSpeechToText
IModule <|.. AzureTextToSpeech
IModule <|.. Email
ModuleProvider ..> IModule : returns selected strategy
ModuleEndpoints --> ModuleProvider : usesRuntime Sequence
sequenceDiagram
participant Client
participant Endpoint as ModuleEndpoints
participant Provider as ModuleProvider
participant Strategy as IModule (concrete)
Client->>Endpoint: POST /strategies/{name}
Endpoint->>Provider: Create(name)
Provider->>Provider: Validate name
Provider->>Provider: Lookup concrete module type
Provider->>Provider: Resolve instance via DI
Provider-->>Endpoint: IModule
Endpoint->>Strategy: Execute()
Endpoint-->>Client: 200 OKSelection And Discovery Flow
flowchart LR
A[Application Startup] --> B[Scan assembly for classes implementing IModule]
B --> C[Build dictionary keyed by type name]
C --> D[Freeze lookup for runtime reads]
E[POST /strategies/#123;name#125;] --> F[ModuleProvider.Create#40;name#41;]
F --> G[Name valid and found?]
G -- No --> H[Throw argument/key exception]
G -- Yes --> I[Create instance through DI]
I --> J[Return IModule]
J --> K[Execute selected strategy]Benefits In This Project
Extensibility with minimal API changes. Adding a new module strategy only requires implementing
IModulein the Domain assembly; endpoint flow can remain unchanged.Low coupling between transport and behavior.
ModuleEndpointsdoes not depend on concrete module classes and only coordinates runtime selection and execution.Centralized strategy creation rules.
ModuleProviderowns validation, lookup, and construction concerns in one place.Better testability. You can test each strategy implementation independently and test provider selection behavior separately.
Dependency injection support. Strategies can request dependencies in constructors, and
ModuleProviderresolves them consistently through the service provider.
Current Constraints
- Strategy selection key is currently the concrete class name.
- Discovery scans the whole assembly and includes classes assignable to
IModule. - Strategy execution is synchronous through
Execute().
If needed later, these can evolve to explicit strategy metadata, constrained discovery, or async execution.
Code Examples
The following snippets show the Strategy pattern as implemented in this project.
1. Strategy Contract
Each strategy implements a shared interface.
namespace Imparta.iCoachAi.Api.Features.Modules.Domain;
public interface IModule
{
void Execute();
}2. Concrete Strategy Implementations
Each concrete module provides its own behavior behind the same Execute() contract.
namespace Imparta.iCoachAi.Api.Features.Modules.Domain;
public class Email(ILogger<Email> logger) : IModule
{
public void Execute()
{
logger.LogWarning(
"Executing Email module. I am processing {EmailCount} emails. For {emotion}.",
42,
"fun"
);
}
}3. Strategy Selection Context (ModuleProvider)
ModuleProvider discovers, validates, and instantiates strategies by name.
namespace Imparta.iCoachAi.Api.Features.Modules.Domain;
using System.Collections.Frozen;
public sealed class ModuleProvider(IServiceProvider serviceProvider)
{
private static readonly IReadOnlyDictionary<string, Type> ModuleTypes = typeof(IModule)
.Assembly.GetTypes()
.Where(type =>
type is { IsClass: true, IsAbstract: false } && typeof(IModule).IsAssignableFrom(type)
)
.ToDictionary(type => type.Name, type => type, StringComparer.OrdinalIgnoreCase);
private static readonly FrozenDictionary<string, Type> FrozenModuleTypes =
ModuleTypes.ToFrozenDictionary();
public IEnumerable<string> AvailableModules =>
ModuleTypes.Keys.Order(StringComparer.OrdinalIgnoreCase);
public IModule Create(string moduleName)
{
if (string.IsNullOrWhiteSpace(moduleName))
{
throw new ArgumentException("Module name is required.", nameof(moduleName));
}
var moduleType =
FrozenModuleTypes
.FirstOrDefault(mt => mt.Key.Equals(moduleName, StringComparison.OrdinalIgnoreCase))
.Value
?? throw new KeyNotFoundException($"Module '{moduleName}' was not found.");
return (IModule)ActivatorUtilities.CreateInstance(serviceProvider, moduleType);
}
}4. API Endpoints Using Runtime Strategy Selection
The endpoint asks ModuleProvider for the chosen strategy and executes it.
var module = moduleProvider.Create(name);
module.Execute();private static async Task<IResult> GetAvailableStrategies(ModuleProvider moduleProvider)
{
return TypedResults.Ok(moduleProvider.AvailableModules);
}
[Timer]
private static IResult ExecuteStrategy(string name, ModuleProvider moduleProvider)
{
try
{
var module = moduleProvider.Create(name);
module.Execute();
return TypedResults.Ok(new { Strategy = name, Message = "Executed." });
}
catch (KeyNotFoundException ex)
{
return TypedResults.NotFound(new { ex.Message });
}
catch (ArgumentException ex)
{
return TypedResults.BadRequest(new { ex.Message });
}
}5. Feature Registration That Enables Strategy Resolution
The registration below is what makes strategy discovery and runtime creation work.
public static void AddFeatureServices(this IServiceCollection services)
{
services.Scan(scan =>
scan.FromAssemblyOf<IModule>()
.AddClasses(classes => classes.AssignableTo<IModule>())
.AsImplementedInterfaces()
.WithScopedLifetime()
);
services.AddSingleton<ModuleProvider>();
}6. Quick End-To-End Usage
Request available strategies:
GET /strategiesExecute one strategy by name:
POST /strategies/Email7. Unit Test Example (xUnit)
This test validates two important behaviors:
- A known strategy name resolves to an
IModuleimplementation. - An unknown strategy name throws
KeyNotFoundException.
using Imparta.iCoachAi.Api.Features.Modules.Domain;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Imparta.iCoachAi.Api.Tests.Features.Modules.Domain;
public class ModuleProviderTests
{
[Fact]
public void Create_WhenNameExists_ReturnsStrategyInstance()
{
// Arrange
var services = new ServiceCollection();
services.AddLogging();
var provider = services.BuildServiceProvider();
var sut = new ModuleProvider(provider);
// Act
var module = sut.Create("Email");
// Assert
Assert.NotNull(module);
Assert.IsAssignableFrom<IModule>(module);
}
[Fact]
public void Create_WhenNameDoesNotExist_ThrowsKeyNotFoundException()
{
// Arrange
var services = new ServiceCollection();
services.AddLogging();
var provider = services.BuildServiceProvider();
var sut = new ModuleProvider(provider);
// Act + Assert
Assert.Throws<KeyNotFoundException>(() => sut.Create("DoesNotExist"));
}
}If you prefer a stricter test, assert the concrete type as well:
Assert.IsType<Email>(module);