Skip to content

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

  • IModule defines the common operation: Execute().

Concrete Strategies

Current strategy implementations in the Domain folder:

  • AzureSpeechToText
  • AzureTextToSpeech
  • Email

Each class implements IModule and provides its own Execute() behavior.

Context / Selector

  • ModuleProvider is 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 /strategies returns available strategy names.
  • POST /strategies/{name} selects the strategy by name and executes Execute().

Structural Diagram

mermaid
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 : uses

Runtime Sequence

mermaid
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 OK

Selection And Discovery Flow

mermaid
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

  1. Extensibility with minimal API changes. Adding a new module strategy only requires implementing IModule in the Domain assembly; endpoint flow can remain unchanged.

  2. Low coupling between transport and behavior. ModuleEndpoints does not depend on concrete module classes and only coordinates runtime selection and execution.

  3. Centralized strategy creation rules. ModuleProvider owns validation, lookup, and construction concerns in one place.

  4. Better testability. You can test each strategy implementation independently and test provider selection behavior separately.

  5. Dependency injection support. Strategies can request dependencies in constructors, and ModuleProvider resolves 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.

csharp
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.

csharp
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.

csharp
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.

csharp
    var module = moduleProvider.Create(name);
    module.Execute();
csharp
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.

csharp
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:

http
GET /strategies

Execute one strategy by name:

http
POST /strategies/Email

7. Unit Test Example (xUnit)

This test validates two important behaviors:

  • A known strategy name resolves to an IModule implementation.
  • An unknown strategy name throws KeyNotFoundException.
csharp
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:

csharp
Assert.IsType<Email>(module);