2

Akka.NET Dependency Injection Best Practices

 1 year ago
source link: https://petabridge.com/blog/akkadotnet-dependencyinjection/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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:

  1. To bind a DI container to an ActorSystem, we had to create an external IDependencyResolver that accepted an ActorSystem and a DI kernel together… And then store it somewhere, which was awkward;
  2. It was theoretically possible to change the IDependencyResolver instance at runtime, which is inherently unsafe in a concurrent system;
  3. Managing NuGet packages for ~10-12 different DI containers is a nuisance; and
  4. 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:

  1. Have actors who require dependency injection take an IServiceProvider via their constructor arguments;
  2. Create an IServiceScope inside each actor instance;
  3. Create all of your dependencies you require through the IServiceScope; and
  4. Dispose of your IServiceScope inside your ActorBase.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!
Written by Aaron Stannard on January 27, 2021

Monitor Your Akka.NET Applications with Ease

Get to the cutting edge with Akka.NET

Learn production best practices, operations and deployment approaches for using Akka.NET.


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK