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