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.

Standalone DI Package

The DI system is available as a separate NuGet package for projects that only need dependency injection — no server, no networking, no gaming:

dotnet add package Altruist.DI --version 0.9.6-beta
using Altruist;

await AltruistDI.Run(args);

This gives you everything on this page: [Service], [PostConstruct], [ConditionalOnConfig], [AppConfigValue], config binding — without pulling in the full framework.

Note:

The full framework (dotnet add package Altruist) includes Altruist.DI automatically. You only need the standalone package if your project doesn't use Altruist's server, networking, or gaming features — for example a background worker, CLI tool, or microservice that just needs config-driven DI.

What AltruistDI.Run does

  1. Loads config.yml (same config system as the full framework)
  2. Scans all assemblies for [Service] classes
  3. Resolves dependencies and registers them in the DI container
  4. Binds [ConfigurationProperties] classes to config sections
  5. Runs all [PostConstruct] methods

After Run() completes, all your services are wired and initialized. The full framework (AltruistApplication.Run) calls AltruistDI.Run internally and then adds portals, transports, and modules on top.

Resolving Services

Always prefer constructor injection — the framework wires everything automatically:

[Service]
public class MyWorker
{
    private readonly IMyService _service;

    public MyWorker(IMyService service)
    {
        _service = service; // Injected by the framework
    }
}

If you can't use constructor injection (e.g., in a static method or a context where DI isn't available), use Dependencies.Inject<T>():

// Only when constructor injection isn't possible
var service = Dependencies.Inject<IMyService>();
await service.DoWork();

Note:

Dependencies.Inject<T>() is a service locator — use it sparingly. Constructor injection is always preferred because it makes dependencies explicit and testable. Inject<T>() exists for edge cases like static helpers, interop code, or manual factory methods where DI can't reach.


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>();
    }
}

Optional and Default Dependencies

If a constructor parameter has a default value, Altruist uses that default when the dependency isn't registered — instead of throwing:

[Service]
public class GameServer
{
    public GameServer(
        IAltruistRouter router,                          // Required — fails if missing
        IVisibilityTracker? visibilityTracker = null,    // Optional — null if not registered
        ICombatService? combat = null,                   // Optional — null if not registered
        int maxPlayers = 100)                            // Default value — always 100 (use [AppConfigValue] for config)
    {
        // visibilityTracker is null if gaming module isn't loaded
        // combat is null if combat module isn't loaded
        // maxPlayers is 100 unless overridden
    }
}
Parameter styleDependency missingResult
IMyService serviceNot registeredStartup fails with error
IMyService? service = nullNot registeredReceives null
IMyService service = nullNot registeredReceives null
int timeout = 30No matching serviceReceives 30

Note:

This is how Altruist handles optional modules. The framework's own services use this pattern extensively — for example, GameWorldOrganizer3D takes IVisibilityTracker? visibilityTracker = null because visibility tracking is optional. Your services can do the same.

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