# 12 — Dependency Injection (Akka.DependencyInjection) ## Overview Akka.DependencyInjection integrates `Microsoft.Extensions.DependencyInjection` with Akka.NET actor construction, allowing actors to receive injected services through their constructors. When using Akka.Hosting (recommended), DI integration is handled automatically — `Akka.DependencyInjection` is the underlying mechanism. In the SCADA system, DI is essential for injecting protocol adapters, configuration services, logging, and database clients into actors without tight coupling. ## When to Use - Injecting protocol adapter factories (OPC-UA client factory, custom protocol client factory) into device actors - Injecting configuration services (`IOptions`) into the Device Manager - Injecting logging (`ILogger`) into actors for structured logging - Injecting database clients or repository services for historian writes ## When Not to Use - Do not inject `IActorRef` via standard DI — use the `ActorRegistry` and `IRequiredActor` from Akka.Hosting instead - Do not inject heavy, stateful services that have their own lifecycle management conflicts with actor lifecycle - Do not use DI to inject mutable shared state — this breaks actor isolation ## Design Decisions for the SCADA System ### Actor Construction via DI Resolver Use Akka.Hosting's `resolver.Props()` to create actors with DI-injected constructors: ```csharp akkaBuilder.WithActors((system, registry, resolver) => { // DeviceManagerActor receives IOptions and // IProtocolAdapterFactory via its constructor var props = resolver.Props(); var manager = system.ActorOf(props, "device-manager"); registry.Register(manager); }); ``` ### Protocol Adapter Injection The unified protocol abstraction is implemented via a factory pattern. Register the factory in DI, inject it into the Device Manager, which then creates the appropriate adapter actor per device: ```csharp // DI registration builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.AddTransient(); // Device Manager actor constructor public class DeviceManagerActor : ReceiveActor { public DeviceManagerActor( IOptions config, IProtocolAdapterFactory adapterFactory, IServiceProvider serviceProvider) { // Use adapterFactory to create protocol-specific adapters // Use serviceProvider for creating scoped services } } ``` ### Scoped Services and Actor Lifecycle Actors are long-lived. DI scopes must be managed manually to avoid memory leaks with scoped/transient services: ```csharp public class DeviceActor : ReceiveActor { private readonly IServiceScope _scope; private readonly IHistorianWriter _historian; public DeviceActor(IServiceProvider sp, DeviceConfig config) { _scope = sp.CreateScope(); _historian = _scope.ServiceProvider.GetRequiredService(); Receive(HandleTagUpdate); } protected override void PostStop() { _scope.Dispose(); // Clean up scoped services base.PostStop(); } } ``` ### Child Actor Creation with DI When the Device Manager creates child device actors that also need DI services, use the `IDependencyResolver`: ```csharp // Inside DeviceManagerActor var resolver = DependencyResolver.For(Context.System); var deviceProps = resolver.Props(deviceConfig); // Additional args var deviceActor = Context.ActorOf(deviceProps, $"device-{deviceConfig.DeviceId}"); ``` ## Common Patterns ### Factory Pattern for Protocol Selection ```csharp public interface IProtocolAdapterFactory { Props CreateAdapterProps(DeviceConfig config, IDependencyResolver resolver); } public class ProtocolAdapterFactory : IProtocolAdapterFactory { public Props CreateAdapterProps(DeviceConfig config, IDependencyResolver resolver) { return config.Protocol switch { ProtocolType.OpcUa => resolver.Props(config), ProtocolType.Custom => resolver.Props(config), _ => throw new ArgumentException($"Unknown protocol: {config.Protocol}") }; } } ``` ### ILogger Integration Inject `ILoggerFactory` and create loggers inside actors: ```csharp public class DeviceActor : ReceiveActor { private readonly ILogger _logger; public DeviceActor(ILoggerFactory loggerFactory, DeviceConfig config) { _logger = loggerFactory.CreateLogger($"Device.{config.DeviceId}"); _logger.LogInformation("Device actor started for {DeviceId}", config.DeviceId); } } ``` ## Anti-Patterns ### Injecting IActorRef Directly via DI Do not register `IActorRef` in the DI container. Actor references are runtime constructs managed by the ActorSystem. Use `ActorRegistry` and `IRequiredActor` instead. ### Singleton Services with Mutable State If a DI singleton service has mutable state, injecting it into multiple actors creates shared mutable state — violating actor isolation. Either make the service thread-safe (with locks) or restructure it as an actor. ### Forgetting to Dispose Scopes If an actor creates an `IServiceScope` and doesn't dispose it in `PostStop`, scoped services (database connections, HTTP clients) leak. Always pair scope creation with disposal. ### Constructor Doing Heavy Work Actor constructors should be lightweight. Do not perform I/O (network connections, database queries) in the constructor. Use `PreStart` for initialization that requires async work. ## Configuration Guidance No additional HOCON or Hosting configuration is needed beyond the standard Akka.Hosting setup. DI integration is enabled automatically when using `AddAkka()` with `resolver.Props()`. ### Service Lifetimes | Service | Recommended Lifetime | Reason | |---|---|---| | `IProtocolAdapterFactory` | Singleton | Stateless factory, shared across actors | | `OpcUaClientWrapper` | Transient | One per device actor, owned by the actor | | `IHistorianWriter` | Scoped | Tied to actor lifecycle via `IServiceScope` | | `IOptions` | Singleton | Configuration doesn't change at runtime | | `ILoggerFactory` | Singleton | Standard .NET pattern | ## References - Official Documentation: - Akka.Hosting DI Integration: