Skip to content

Module System

Overview

The LBS module system provides a declarative way to enable/disable features and timers based on enabled modules. It integrates seamlessly with the Timer Infrastructure to automatically enable/disable timers based on module requirements.

Core Concepts

Modules

Modules represent features or integrations in the system. Examples: - NrlSuperCoach - NRL SuperCoach fantasy integration - BallrContests - Ballr contest functionality - DiscordIntegration - Discord bot integration - StripeIntegration - Payment processing

Module Dependencies

Modules can depend on other modules: - NrlSuperCoach requires Fantasy + NrlApiIntegration - BallrPayouts requires Ballr + BallrContests + StripeIntegration

Dependencies are automatically enabled when you enable a module.

Module Categories

Core Modules - Always enabled: - Core, Admin, User, Logging

Fantasy Modules: - Fantasy, NrlSuperCoach, AflSuperCoach, DreamTeam

Ballr Modules: - Ballr, BallrContests, BallrLeaderboards, BallrPayouts

Integration Modules: - NrlApiIntegration, AflApiIntegration, DiscordIntegration, StripeIntegration

Registration

Basic Setup

using LBS.Augment.Modules;

// In Program.cs - Add module infrastructure
builder.Services.AddModuleInfrastructure();

Setup with Initial Modules

// Enable specific modules on startup
builder.Services.AddModuleInfrastructure(
    ModuleDefinition.Fantasy,
    ModuleDefinition.NrlSuperCoach,
    ModuleDefinition.Ballr);

Setup with Configuration

builder.Services.AddModuleInfrastructure(provider =>
{
    // Enable modules based on config
    provider.EnableModule(ModuleDefinition.NrlSuperCoach);
    provider.EnableModule(ModuleDefinition.BallrContests);
});

Using RequiresModule with Timers

Basic Timer with Module Requirement

using LBS.Augment.Modules;
using LBS.Augment.Timers;

public class NrlSuperCoachTimers
{
    [Timer(maxConcurrency: 1, intervalInSeconds: TimerInterval.FiveMinutes)]
    [RequiresModule(ModuleDefinition.NrlSuperCoach)]
    public async Task ImportPlayerStatsAsync(CancellationToken cancellationToken)
    {
        // Only runs when NrlSuperCoach module is enabled
        await Task.CompletedTask;
    }
}

How it works: - Timer is discovered on startup - If NrlSuperCoach module is NOT enabled, timer won't run - When module is enabled, timer automatically starts - When module is disabled, timer stops and running instances are cancelled

Multiple Module Requirements (OR Logic)

[Timer(maxConcurrency: 1, intervalInSeconds: TimerInterval.TenMinutes)]
[RequiresModule(ModuleDefinition.NrlSuperCoach)]
[RequiresModule(ModuleDefinition.AflSuperCoach)]
public async Task ProcessFantasyDataAsync(CancellationToken cancellationToken)
{
    // Runs if EITHER NrlSuperCoach OR AflSuperCoach is enabled
}

Multiple Modules in One Requirement (AND Logic)

[Timer(maxConcurrency: 1, intervalInSeconds: TimerInterval.FiveMinutes)]
[RequiresModule(ModuleDefinition.Ballr, ModuleDefinition.StripeIntegration)]
public async Task ProcessPayoutsAsync(CancellationToken cancellationToken)
{
    // Runs ONLY if BOTH Ballr AND StripeIntegration are enabled
}

Excluding Modules

[Timer(maxConcurrency: 1, intervalInSeconds: TimerInterval.Hourly)]
[RequiresModule(ModuleDefinition.Fantasy, ExcludedModule = ModuleDefinition.AdvancedStats)]
public async Task BasicStatsOnlyAsync(CancellationToken cancellationToken)
{
    // Runs when Fantasy is enabled BUT AdvancedStats is NOT enabled
}

Class-Level Requirements

[RequiresModule(ModuleDefinition.Discord)]
public class DiscordTimers
{
    [Timer(maxConcurrency: 1, intervalInSeconds: TimerInterval.OneMinute)]
    public async Task SendNotificationsAsync(CancellationToken cancellationToken)
    {
        // Class-level requirement applies to all methods
    }

    [Timer(maxConcurrency: 1, intervalInSeconds: TimerInterval.TenMinutes)]
    public async Task SyncChannelsAsync(CancellationToken cancellationToken)
    {
        // Also requires Discord module
    }
}

Dynamic Module Management

Enable/Disable Modules at Runtime

public class ModuleManagementService
{
    private readonly IModuleProvider moduleProvider;
    private readonly ILogger<ModuleManagementService> logger;

    public ModuleManagementService(IModuleProvider moduleProvider, ILogger<ModuleManagementService> logger)
    {
        this.moduleProvider = moduleProvider;
        this.logger = logger;
    }

    public void EnableNrlIntegration()
    {
        var provider = (ModuleProvider)this.moduleProvider;

        // Enable module (automatically enables dependencies)
        provider.EnableModule(ModuleDefinition.NrlSuperCoach);

        // Timers with [RequiresModule(ModuleDefinition.NrlSuperCoach)] will start automatically
        this.logger.LogInformation("NRL SuperCoach enabled");
    }

    public void DisableNrlIntegration()
    {
        var provider = (ModuleProvider)this.moduleProvider;

        // Disable module (automatically disables dependent modules)
        provider.DisableModule(ModuleDefinition.NrlSuperCoach);

        // Timers with [RequiresModule(ModuleDefinition.NrlSuperCoach)] will stop automatically
        this.logger.LogInformation("NRL SuperCoach disabled");
    }
}

Check Module Status

public class FeatureService
{
    private readonly IModuleProvider moduleProvider;

    public bool IsNrlEnabled()
    {
        return this.moduleProvider.IsModuleEnabled(ModuleDefinition.NrlSuperCoach);
    }

    public bool ArePayoutsAvailable()
    {
        return this.moduleProvider.AreModulesEnabled(
            ModuleDefinition.Ballr,
            ModuleDefinition.BallrPayouts,
            ModuleDefinition.StripeIntegration);
    }
}

Listen for Module Changes

public class ModuleChangeListener
{
    private readonly IModuleProvider moduleProvider;

    public ModuleChangeListener(IModuleProvider moduleProvider)
    {
        this.moduleProvider = moduleProvider;
        this.moduleProvider.ModulesChanged += this.OnModulesChanged;
    }

    private void OnModulesChanged(object? sender, ModulesChangedEventArgs e)
    {
        Console.WriteLine($"Modules changed:");
        Console.WriteLine($"  Enabled: {string.Join(", ", e.EnabledModules)}");
        Console.WriteLine($"  Added: {string.Join(", ", e.AddedModules)}");
        Console.WriteLine($"  Removed: {string.Join(", ", e.RemovedModules)}");
    }
}

Complete Example

Program.cs Setup

using LBS.Augment.Modules;
using LBS.Augment.Timers;

var builder = WebApplication.CreateBuilder(args);

// 1. Add module infrastructure
builder.Services.AddModuleInfrastructure(provider =>
{
    // Enable modules based on configuration
    var enableNrl = builder.Configuration.GetValue<bool>("Features:NrlSuperCoach");
    var enableBallr = builder.Configuration.GetValue<bool>("Features:Ballr");

    if (enableNrl)
        provider.EnableModule(ModuleDefinition.NrlSuperCoach);

    if (enableBallr)
        provider.EnableModule(ModuleDefinition.Ballr);
});

// 2. Add timer infrastructure (module provider auto-injected)
builder.Services.AddTimerInfrastructure(typeof(Program).Assembly);

var app = builder.Build();

// 3. Refresh timer module status after discovery
using (var scope = app.Services.CreateScope())
{
    var timerService = scope.ServiceProvider.GetRequiredService<TimerService>();
    timerService.RefreshTimerModuleStatus();
}

app.Run();

Timer Implementation

[RequiresModule(ModuleDefinition.NrlSuperCoach)]
public class NrlSuperCoachTimers
{
    private readonly ILogger<NrlSuperCoachTimers> logger;
    private readonly IServiceProvider serviceProvider;

    public NrlSuperCoachTimers(ILogger<NrlSuperCoachTimers> logger, IServiceProvider serviceProvider)
    {
        this.logger = logger;
        this.serviceProvider = serviceProvider;
    }

    [Timer(
        maxConcurrency: 1,
        intervalInSeconds: TimerInterval.FiveMinutes,
        retryOnError: true)]
    public async Task ImportPlayerStatsAsync(CancellationToken cancellationToken)
    {
        using var scope = this.serviceProvider.CreateScope();
        var nrlApiClient = scope.ServiceProvider.GetRequiredService<INrlApiClient>();

        this.logger.LogInformation("Importing NRL player stats...");

        var stats = await nrlApiClient.GetPlayerStatsAsync(cancellationToken);

        // Process stats...

        this.logger.LogInformation("Imported {Count} player stats", stats.Count);
    }

    [Timer(
        maxConcurrency: 1,
        intervalInSeconds: TimerInterval.TenMinutes)]
    [BlackoutPeriod(2, 0, 6, 0, new[] { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday })]
    public async Task SyncLeaderboardsAsync(CancellationToken cancellationToken)
    {
        // Only runs when NRL module enabled, and outside 2 AM - 6 AM
        this.logger.LogInformation("Syncing leaderboards...");
        await Task.CompletedTask;
    }
}

Module Definition Management

Adding New Modules

Edit ModuleDefinition.cs:

public static class ModuleDefinition
{
    // Add new module constant
    public const string MyNewFeature = "MyNewFeature";

    // Add to appropriate category
    public static readonly string[] FeatureModules =
    {
        MyNewFeature
    };

    // Define dependencies if needed
    private static readonly Dictionary<string, string[]> ModuleDependencies = new()
    {
        { MyNewFeature, new[] { Core, SomeOtherModule } }
    };
}

Module Best Practices

  1. Use constants - Always use ModuleDefinition constants, never hardcode strings
  2. Declare dependencies - Use dependency system rather than manual checks
  3. Core modules - Never disable core modules programmatically
  4. Timer granularity - Create separate timers per feature for better module isolation
  5. Test module changes - Verify timers start/stop correctly when modules change

Architecture

Timer + Module Integration Flow

  1. App Startup - AddModuleInfrastructure() registers IModuleProvider
  2. Timer Discovery - AddTimerInfrastructure() discovers timers with [Timer] attribute
  3. Module Check - RefreshTimerModuleStatus() evaluates [RequiresModule] on each timer
  4. Runtime - Timer loop skips disabled timers
  5. Module Change - ModulesChanged event triggers timer enable/disable
  6. Cancellation - Running instances of disabled timers are cancelled

Performance

  • Module checks - Cached per timer, only reevaluated on module changes
  • Zero overhead - Timers without [RequiresModule] have no performance impact
  • Dependency resolution - Computed once on startup

Troubleshooting

Timer Not Running

Check: 1. Is the required module enabled? moduleProvider.IsModuleEnabled(module) 2. Are dependencies satisfied? Check ModuleDefinition.GetModuleWithDependencies(module) 3. Is timer in blackout period? 4. Has max concurrency been reached?

Module Won't Enable

Check: 1. Are dependencies enabled? ModuleDefinition.ValidateModuleDependencies() 2. Are incompatible modules enabled? ModuleDefinition.GetIncompatibleModules()

Timer Keeps Running After Module Disabled

  1. Verify ModulesChanged event is firing
  2. Check RefreshTimerModuleStatus() was called after timer discovery
  3. Ensure IModuleProvider is registered as singleton

See Also