Akka.NET, ASP.NET Core, Hosted Services, and Dependency Injection

Akka.Hosting trivializes integrating Akka.NET with everything else in .NET.

We first introduced Akka.Hosting a couple of years ago and released it to market one of the major pillars of Akka.NET v1.5. Without any exaggeration, it is the single best usability improvement we have ever made to Akka.NET and we are furiously rewriting all of the official Akka.NET documentation and training courses to prioritize it.

In that same vein, we’re going to introduce how you can use Akka.Hosting to easily integration Akka.NET with:

  • Microsoft.Extensions.DependencyInjection - injecting things into actors and actors into things;
  • ASP.NET Core / SignalR / gRPC / etc - anything that can be injected via MSFT.EXT.DI really; and
  • Running Akka.NET in headless IHostedServices for things like stand-alone Windows Services.

Read on.

Integrating Akka.NET with Microsoft.Extensions.DependencyInjection

Akka.Hosting leverages Akka.DependencyInjection in order to make it possible to start actors with a blend of DI-injected and non-injected arguments from the IServiceProvider.

I covered all of the guts of this in our new video, “Everything You Wanted to Know about Dependency Injection and Akka.NET

However, a simple example using Akka.Hosting (from Akka.Templates, which you should install):

using Akka.Hosting;
using AkkaConsoleTemplate.App;
using Microsoft.Extensions.Hosting;

var hostBuilder = new HostBuilder();

hostBuilder.ConfigureServices((context, services) =>
{
    services.AddAkka("MyActorSystem", (builder, sp) =>
    {
        builder
            .WithActors((system, registry, resolver) =>
            {
                var helloActor = system.ActorOf(Props.Create(() => new HelloActor()), "hello-actor");
                registry.Register<HelloActor>(helloActor);
            })
            .WithActors((system, registry, resolver) =>
            {
                var timerActorProps =
                    resolver.Props<TimerActor>(); // uses Msft.Ext.DI to inject reference to helloActor
                var timerActor = system.ActorOf(timerActorProps, "timer-actor");
                registry.Register<TimerActor>(timerActor);
            });
    });
});

var host = hostBuilder.Build();

await host.RunAsync();

In this instance, we aren’t directly registering any services aside from Akka.NET itself using the IServiceCollection - the AddAkka method is how we register our ActorSystem, ActorRegistry, and a background IHostedService that will be responsible for managing the lifecycle of your actors and the ActorSystem inside the .NET IHost.

However, take note of the following call:

.WithActors((system, registry, resolver) =>
{
	// IActorRef
    var helloActor = system.ActorOf(Props.Create(() => new HelloActor()), "hello-actor");

    // registers IActorRef with key `HelloActor`
    registry.Register<HelloActor>(helloActor);
})

This registers the IActorRef helloActor into the ActorRegistry, which makes this type accessible via the IRequiredActor<TKey dependency injection service:

public class TimerActor : ReceiveActor, IWithTimers
{
    private readonly IActorRef _helloActor;

    public TimerActor(IRequiredActor<HelloActor> helloActor)
    {
        _helloActor = helloActor.ActorRef;
        Receive<string>(message =>
        {
            _helloActor.Tell(message);
        });
    }

    protected override void PreStart()
    {
        Timers.StartPeriodicTimer("hello-key", "hello", TimeSpan.FromSeconds(1));
    }

    public ITimerScheduler Timers { get; set; } = null!; // gets set by Akka.NET
}

In this instance, the constructor of the TimerActor takes a DI’d argument of IRequiredActor<HelloActor> - and this syntax will also work for ASP.NET Core, SignalR, gRPC, IHostedService, and more - which we’ll see a bit later.

The ActorRegistry is an Akka.Hosting-specific construct aimed at making it easier to inject actors directly into non-actor things, but it also works perfectly well with Akka.DependencyInjection and actors that can be started via DI.

And here’s how we do just that - inject DI’d arguments into actors:

.WithActors((system, registry, resolver) =>
{
    var timerActorProps =
        resolver.Props<TimerActor>(); // uses Msft.Ext.DI to inject reference to helloActor
    var timerActor = system.ActorOf(timerActorProps, "timer-actor");
    registry.Register<TimerActor>(timerActor);
});

The resolver argument corresponds to the DependencyResolver from Akka.DependencyInjector, a razor-thin wrapper around the IServiceProvider - we simply use it to create the Props<TimerActor> and that will cause this actor to resolve any arguments not specified inside the Props() method from the IServiceProvider directly.

Integrating Akka.NET with ASP.NET Core

Once you’ve gotten onboard with Akka.Hosting, integrating Akka.NET with ASP.NET Core is trivial - as I show in “Using Akka.NET and ASP.NET Core Together

This bit of code comes from our “SQL Sharding” example project - a Razor Pages application that uses Akka.Cluster.Sharding and Akka.Cluster Singletons to communicate with another service running in a separate process:


var builder = WebApplication.CreateBuilder(args);

// [config stuff]

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddAkka("SqlSharding", (configurationBuilder, provider) =>
{
    configurationBuilder.WithRemoting(hostName, port)
        .AddAppSerialization()
        .WithClustering(new ClusterOptions
            { Roles = new[] { "Web" }, SeedNodes = seeds })
        .WithShardRegionProxy<ProductMarker>("products", ProductActorProps.SingletonActorRole,
            new ProductMessageRouter())
        .WithSingletonProxy<ProductIndexMarker>("product-proxy",
            new ClusterSingletonOptions { Role = ProductActorProps.SingletonActorRole });
        // [other actor registrations]
});

Same setup as what we did before, but with a minimal WebApplication setup instead.

If we take a look at our corresponding Razor Page PageModel:

public class Product : PageModel
{
    private readonly IActorRef _productActor;

    public Product(IRequiredActor<ProductMarker> productActor)
    {
        _productActor = productActor.ActorRef;
    }

    [BindProperty(SupportsGet = true)]
    public string ProductId { get; set; }
    
    [BindProperty]
    [Required]
    [Range(1, 10000)]
    public int Quantity { get; set; }
    
    public ProductState State { get; set; }

    public async Task<IActionResult> OnGetAsync()
    {
        var result = await _productActor.Ask<FetchResult>(new FetchProduct(ProductId), TimeSpan.FromSeconds(3));
        State = result.State;

        if (State.IsEmpty) // no product with this id
            return NotFound();

        return Page();
    }
}

The Product page model takes an IRequiredActor<ProductMarker> in its constructor, which will be resolved via ASP.NET Core’s IServiceProvider at the time an HTTP request is sent to the corresponding route for this particular page. The ProductMarker corresponds to the ShardRegionProxy for the products entity actors hosted in the other service. My ASP.NET resources can just interact with those actors no problem when we have Akka.Hosting hooked up to the IServiceCollection.

Running Akka.NET as a Headless Service with Microsoft.Extensions.Hosting

Suppose you want to be able to run Akka.NET as a Windows Service, Linux Service, or just a headless process inside a platform like Kubernetes? Combining Akka.Hosting with Microsoft.Extensions.Hosting is the best way to go about that. I covered this at full length in “Building Headless Akka.NET Services.”

Our original Akka.DependencyInjection code sample illustrated a basic headless Akka.NET service quite well:

using Akka.Hosting;
using AkkaConsoleTemplate.App;
using Microsoft.Extensions.Hosting;

var hostBuilder = new HostBuilder();

hostBuilder.ConfigureServices((context, services) =>
{
    services.AddAkka("MyActorSystem", (builder, sp) =>
    {
        builder
            .WithActors((system, registry, resolver) =>
            {
                var helloActor = system.ActorOf(Props.Create(() => new HelloActor()), "hello-actor");
                registry.Register<HelloActor>(helloActor);
            })
            .WithActors((system, registry, resolver) =>
            {
                var timerActorProps =
                    resolver.Props<TimerActor>(); // uses Msft.Ext.DI to inject reference to helloActor
                var timerActor = system.ActorOf(timerActorProps, "timer-actor");
                registry.Register<TimerActor>(timerActor);
            });
    });
});

var host = hostBuilder.Build();

await host.RunAsync();

This will run Akka.NET using the default IHostedService launched as part of Akka.Hosting’s AddAkka method, but you can also run Akka.NET inside BackgroundServices and other custom IHostedService implementations too:

public class MyBackgroundService : BackgroundService
{
    private readonly IRequiredActor<TestActorKey> _testActor;

    public MyBackgroundService(IRequiredActor<TestActorKey> requiredActor)
    {
       _testActor = requiredActor;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
    	// asynchronously request the background actor
        var myRef = await _testActor.GetAsync(stoppingToken);
        var result = myRef.Ask<LongRunningOperationResult>(Start.Instance, cancellationToken:stoppingToken);
        // [result handling code]
    }
}

Conclusion

The short version of this post - Akka.Hostingtremendously simplifies Akka.NET, just use it!

One of the best ways to get started with it in a new project is to use our Akka.Templates NuGet package:

dotnet new install "Akka.Templates::*"

This will make all of our templates available for the dotnet new CLI but also as new project templates in Visual Studio, Rider, and VS Code.

If you liked this post, you can share it with your followers or follow us on Twitter!
Written by Aaron Stannard on April 23, 2024

 

 

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.