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

6.5 KiB

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:

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:

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

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:

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

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:

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