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.
177 lines
6.5 KiB
Markdown
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/>
|