# 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/>
