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¶
- Use constants - Always use
ModuleDefinitionconstants, never hardcode strings - Declare dependencies - Use dependency system rather than manual checks
- Core modules - Never disable core modules programmatically
- Timer granularity - Create separate timers per feature for better module isolation
- Test module changes - Verify timers start/stop correctly when modules change
Architecture¶
Timer + Module Integration Flow¶
- App Startup -
AddModuleInfrastructure()registersIModuleProvider - Timer Discovery -
AddTimerInfrastructure()discovers timers with[Timer]attribute - Module Check -
RefreshTimerModuleStatus()evaluates[RequiresModule]on each timer - Runtime - Timer loop skips disabled timers
- Module Change -
ModulesChangedevent triggers timer enable/disable - 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¶
- Verify
ModulesChangedevent is firing - Check
RefreshTimerModuleStatus()was called after timer discovery - Ensure
IModuleProvideris registered as singleton