Services & Dependency Injection
Altruist uses an attribute-based dependency injection system. You annotate classes with [Service] and the framework auto-discovers and registers them — no manual services.AddSingleton<>() calls needed.
Registering a Service
Add [Service] to any class to register it in the DI container:
[Service]
public class GameLogic
{
// Registered as GameLogic (concrete type)
}
Registering Against an Interface
[Service(typeof(IGameLogic))]
public class GameLogic : IGameLogic
{
// Registered as IGameLogic -> GameLogic
}
Service Lifetime
By default, services are Singleton. You can specify a different lifetime:
[Service(typeof(IMyService), ServiceLifetime.Transient)]
public class MyService : IMyService { }
[Service(typeof(IScopedService), ServiceLifetime.Scoped)]
public class ScopedService : IScopedService { }
Constructor Injection
Services can inject other services, config values, and framework components:
[Service(typeof(IEmailService))]
public class SmtpEmailService : IEmailService
{
private readonly SmtpOptions _options;
private readonly ILogger _logger;
public SmtpEmailService(IOptions<SmtpOptions> options, ILoggerFactory loggerFactory)
{
_options = options.Value;
_logger = loggerFactory.CreateLogger<SmtpEmailService>();
}
}
Post-Construction Hooks
Use [PostConstruct] to run initialization logic after the DI container is fully built:
[Service]
public class WorldLoader
{
[PostConstruct]
public async Task Initialize()
{
// Runs after all services are registered and the container is built.
// Safe to resolve other services here.
await LoadWorldData();
}
}
Note:
[PostConstruct] methods can be void, Task, or async Task. They run once after the full bootstrap is complete.
Conditional Registration
Use [ConditionalOnConfig] to register a service only when a config value matches:
// Only registered when transport mode is "websocket"
[Service(typeof(ITransport))]
[ConditionalOnConfig("altruist:server:transport:mode", havingValue: "websocket")]
public class WebSocketTransport : ITransport { ... }
// Only registered when the config section exists (any value)
[Service(typeof(ISocketManager))]
[ConditionalOnConfig("altruist:server:transport")]
public class SocketManager : ISocketManager { ... }
This enables swapping implementations (e.g., WebSocket vs TCP) by changing a single config line.
Conditional on Assembly
Use [ConditionalOnAssembly] to register a service only when a specific assembly is loaded. This is useful for optional plugin-style modules:
[Service(typeof(IAnalytics))]
[ConditionalOnAssembly("MyGame.Analytics")]
public class AnalyticsService : IAnalytics { ... }
Note:
[ConditionalOnAssembly] checks if the named assembly is present in the current AppDomain. If it's not loaded, the service is silently skipped — no errors.
Injecting Config Values
Use [AppConfigValue] on constructor parameters to inject values directly from config.yml:
public class MyService
{
public MyService(
[AppConfigValue("altruist:server:http:port", "8080")] string port,
[AppConfigValue("altruist:server:transport:timeout", "10")] int timeout)
{
// Values resolved from config with fallback defaults
}
}
Modules
For organizing initialization logic, use [AltruistModule] on a static class with [AltruistModuleLoader] methods:
[AltruistModule(Name = "MyGameModule")]
public static class MyGameModule
{
[AltruistModuleLoader]
public static async Task Load(IGameWorldService worldService, ILoggerFactory loggerFactory)
{
// Called after all services are built.
// Parameters are resolved from DI.
var logger = loggerFactory.CreateLogger("MyGameModule");
logger.LogInformation("Game module loaded!");
await worldService.Initialize();
}
}
Resilient Services
Services that connect to external systems (databases, caches, APIs) can implement IConnectable to integrate with Altruist's health monitoring:
[Service(typeof(IConnectable))]
public class MyExternalService : IConnectable
{
public string ServiceName => "MyExternalService";
public bool IsConnected { get; private set; }
public event Action? OnConnected;
public event Action<Exception>? OnFailed;
public event Action<Exception>? OnRetryExhausted;
public async Task ConnectAsync(string protocol, string host, int port,
int maxRetries = 30, int delayMilliseconds = 2000)
{
// Retry connection logic
// Call RaiseConnectedEvent() on success
// Call RaiseOnRetryExhaustedEvent() if all retries fail
}
public Task ConnectAsync() => ConnectAsync("tcp", "localhost", 5432);
public void RaiseConnectedEvent() => OnConnected?.Invoke();
public void RaiseFailedEvent(Exception ex) => OnFailed?.Invoke(ex);
public void RaiseOnRetryExhaustedEvent(Exception ex) => OnRetryExhausted?.Invoke(ex);
}
When registered as IConnectable, Altruist will:
- Attempt to connect the service on startup
- Retry on failure
- Prevent the game engine from starting until all services are healthy
- Gracefully pause during outages