Files
scadalink-design/AkkaDotNet/12-DependencyInjection.md
Joseph Doherty de636b908b Add Akka.NET reference documentation
Notes and documentation covering actors, remoting, clustering, persistence,
streams, serialization, hosting, testing, and best practices for the Akka.NET
framework used throughout the ScadaLink system.
2026-03-16 09:08:17 -04:00

177 lines
6.5 KiB
Markdown

# 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<SiteConfiguration>`) into the Device Manager
- Injecting logging (`ILogger<T>`) 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<T>` 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<T>()` to create actors with DI-injected constructors:
```csharp
akkaBuilder.WithActors((system, registry, resolver) =>
{
// DeviceManagerActor receives IOptions<SiteConfiguration> and
// IProtocolAdapterFactory via its constructor
var props = resolver.Props<DeviceManagerActor>();
var manager = system.ActorOf(props, "device-manager");
registry.Register<DeviceManagerActor>(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<IProtocolAdapterFactory, ProtocolAdapterFactory>();
builder.Services.AddTransient<OpcUaClientWrapper>();
builder.Services.AddTransient<CustomProtocolClientWrapper>();
// Device Manager actor constructor
public class DeviceManagerActor : ReceiveActor
{
public DeviceManagerActor(
IOptions<SiteConfiguration> 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<IHistorianWriter>();
Receive<TagUpdate>(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<OpcUaDeviceActor>(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<OpcUaDeviceActor>(config),
ProtocolType.Custom => resolver.Props<CustomProtocolDeviceActor>(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<T>` 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<T>()`.
### 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<SiteConfiguration>` | Singleton | Configuration doesn't change at runtime |
| `ILoggerFactory` | Singleton | Standard .NET pattern |
## References
- Official Documentation: <https://getakka.net/articles/actors/dependency-injection.html>
- Akka.Hosting DI Integration: <https://petabridge.com/blog/akkadotnet-hosting-aspnet/>