Akka.NET Dependency Injection Best Practices
How to work with the new Akka.DependencyInjection library and Microsoft.Extensions.DependencyInjection
As part of the Akka.NET v1.4.15 release we’ve completely rewritten how dependency injection works in Akka.NET and introduced a new library: Akka.DependencyInjection. In this blog post we’re going to introduce Akka.DependencyInjection and some of the best practices for working with it.
We’ve also produced a brief YouTube video that demonstrates how to work with Akka.DependencyInjection too:
Differences between Akka.DI.Core and Akka.DependencyInjection
Since before even the release of Akka.NET 1.0 we’ve had dependency injection support in the form of the Akka.DI.Core NuGet packages - and all of the other packages that derived from Akka.DI.Core, such as Akka.DI.Autofac, Akka.DI.CastleWindsor, and so forth.
These packages all suffered from a number of shortcomings:
- To bind a DI container to an
ActorSystem
, we had to create an externalIDependencyResolver
that accepted anActorSystem
and a DI kernel together… And then store it somewhere, which was awkward; - It was theoretically possible to change the
IDependencyResolver
instance at runtime, which is inherently unsafe in a concurrent system; - Managing NuGet packages for ~10-12 different DI containers is a nuisance; and
- Mixing and matching dependency injection with non-DI’d constructor arguments was impossible to do.
In addition to all of this - since the release of .NET Core Microsoft.Extensions.DependencyInjection
has become the de facto standard for how dependency injection is instrumented inside most .NET applications and it was about time we added support for that.
Thus Akka.DependencyInjection was born - it solves all of the above issues with the addition of a very simple API.
Given the following container registration:
public void ConfigureServices(IServiceCollection services)
{
// set up a simple service we're going to hash
services.AddScoped<IHashService, HashServiceImpl>();
// creates instance of IPublicHashingService that can be accessed by ASP.NET
services.AddSingleton<IPublicHashingService, AkkaService>();
// starts the IHostedService, which creates the ActorSystem and actors
services.AddHostedService<AkkaService>(sp => (AkkaService)sp.GetRequiredService<IPublicHashingService>());
}
We can easily create actors that depend on and dispose of these interfaces via the ServiceProvider
extenion in Akka.DependencyInjection:
// actor type
public class HasherActor : ReceiveActor
{
private readonly ILoggingAdapter _log = Context.GetLogger();
private readonly IServiceScope _scope;
private readonly IHashService _hashService;
public HasherActor(IServiceProvider sp)
{
_scope = sp.CreateScope();
_hashService = _scope.ServiceProvider.GetRequiredService<IHashService>();
Receive<string>(str =>
{
var hash = _hashService.Hash(str);
Sender.Tell(new HashReply(hash, Self));
});
}
protected override void PostStop()
{
_scope.Dispose();
// _hashService should be disposed once the IServiceScope is disposed too
_log.Info("Terminating. Is ScopedService disposed? {0}", _hashService.IsDisposed);
}
}
// instantion of actor
// props created via IServiceProvider dependency injection
var hasherProps = ServiceProvider.For(_actorSystem).Props<HasherActor>();
RouterActor = _actorSystem.ActorOf(hasherProps.WithRouter(FromConfig.Instance), "hasher");
We just have to make sure that it’s bound to our ActorSystem
during creation via the ServiceProviderSetup
class:
public class AkkaService : IPublicHashingService, IHostedService
{
private ActorSystem _actorSystem;
public IActorRef RouterActor { get; private set; }
private readonly IServiceProvider _sp;
public AkkaService(IServiceProvider sp)
{
_sp = sp;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
var hocon = ConfigurationFactory.ParseString(
await File.ReadAllTextAsync("app.conf", cancellationToken));
var bootstrap = BootstrapSetup.Create().WithConfig(hocon);
var di = ServiceProviderSetup.Create(_sp);
var actorSystemSetup = bootstrap.And(di);
_actorSystem = ActorSystem.Create("AspNetDemo", actorSystemSetup);
// rest of method
}
// rest of class
}
The Akka.DependencyInjection.ServiceProvider
allows us to access the IServiceProvider
from anywhere inside the ActorSystem
, and makes it easy for us to blend DI’d arguments with non-constructor arguments.
public class NonDiArgsActor : ReceiveActor
{
private readonly AkkaDiFixture.ISingletonDependency _singleton;
private readonly IServiceScope _scope;
private AkkaDiFixture.ITransientDependency _transient;
private AkkaDiFixture.IScopedDependency _scoped;
private string _arg1;
private string _arg2;
public NonDiArgsActor(AkkaDiFixture.ISingletonDependency singleton, IServiceProvider sp, string arg1, string arg2)
{
_singleton = singleton;
_scope = sp.CreateScope();
_arg1 = arg1;
_arg2 = arg2;
Receive<FetchDependencies>(_ =>
{
Sender.Tell(new CurrentDependencies(new AkkaDiFixture.IDependency[] { _transient, _scoped, _singleton }));
});
Receive<string>(str =>
{
Sender.Tell(_arg1);
Sender.Tell(_arg2);
});
Receive<Crash>(_ => throw new ApplicationException("crash"));
}
protected override void PreStart()
{
_scoped = _scope.ServiceProvider.GetService<AkkaDiFixture.IScopedDependency>();
_transient = _scope.ServiceProvider.GetRequiredService<AkkaDiFixture.ITransientDependency>();
}
protected override void PostStop()
{
_scope.Dispose();
}
}
So this actor accepts a combination of DI’d and non-DI’d arguments - in the recent past this would not have been possible. However, with Akka.DependencyInjection it is trivial:
var spExtension = ServiceProvider.For(Sys);
var arg1 = "foo";
var arg2 = "bar";
var props = spExtension.Props<NonDiArgsActor>(arg1, arg2);
// create a scoped actor using the props from Akka.DependencyInjection
var scoped1 = Sys.ActorOf(props, "scoped1");
Best Practices for Working with Akka.DependencyInjection
We summarize some of these in the video - but here’s a more complete list.
Akka.DependencyInjection DOES NOT MANAGE THE LIFECYCLE OF YOUR DEPENDENCIES
With Akka.DI.Core we tried to follow the “magic” approach of making sure that your constructor-injected DI resources were automatically disposed for you whenever the actor stopped or restarted. This was met with middling success in practice because each container had totally different and, often, not-threadsafe ways of disposing dependencies.
We aren’t going to bother trying this time around. In order to keep Akka.DependencyInjection simple and efficient the buck is passed to the end user to manage their own dependencies - which should be quite easy to do via the IServiceScope
construct you see in the examples above.
The approach we recommend is:
- Have actors who require dependency injection take an
IServiceProvider
via their constructor arguments; - Create an
IServiceScope
inside each actor instance; - Create all of your dependencies you require through the
IServiceScope
; and - Dispose of your
IServiceScope
inside yourActorBase.PostStop
method.
This is straight forward and will work, especially for actors that need to manage the lifecycle of their dependencies over a long period of time.
Manage Short-Lived, Transient Dependencies on a Per-Call Basis
As we mention frequently in our Akka.NET Design Patterns training course, actors can often live longer than their dependencies - this is especially true of connection-oriented resources such as database clients, which get pooled automatically by the driver when connections are left open for longer than the configured threshold - usually between 30 and 60 seconds.
In these instances, constructor-based dependency injection is a terrible fit for actors that can theoretically live forever. Moreover, you don’t want the lifespan of that IDbConnection
or whatever to be tied to an IServiceScope
that also lives for as long as the actor.
Therefore the best practice is to acquire these dependencies and dispose of them on-the-fly:
Receive<string>(str => {
using(var db = _serviceProvider.GetService<AkkaDiFixture.ITransientDependency>){
// do work
}
});
For an example of how to do this with Entity Framework and the old Akka.DI.Core library, please see “How to use Entity Framework Core with Akka.NET”
We hope you enjoy working with Akka.DependencyInjection - please let us know how we can make it better for you by opening an issue on Github or leaving a comment!
If you liked this post, you can share it with your followers or follow us on Twitter!
- Read more about:
- Akka.NET
- Case Studies
- Videos
Observe and Monitor Your Akka.NET Applications with Phobos
Did you know that Phobos can automatically instrument your Akka.NET applications with OpenTelemetry?
Click here to learn more.