Best Practices for Integrating Akka.NET with ASP.NET Core and SignalR

How to Host Akka.NET Inside ASP.NET Core and SignalR Applications

In the recent past we blogged about hosting headless Akka.NET services using IHostedService and we thought we’d extend that train of thought to cover ASP.NET Core. .NET’s web ecosystem has changed a lot since 2015, when we last wrote about hosting Akka.NET with ASP.NET MVC and even NancyFX on .NET Framework 4.5 - so we’re due for an update.

So, we went ahead and put together a video: “Best Practices for Integrating Akka.NET and ASP.NET Core” which demonstrates how to work with Akka.NET and ASP.NET Core inside a single process.

This blog post accompanies that video and expands on some of the advice we give in it!

NOTE: Also, please check out our Petabridge.App.Web dotnet new template to use these patterns straight away inside your own applications!

Running Akka.NET inside ASP.NET Core

So the ideal formula for running Akka.NET inside ASP.NET Core is to follow the directions from our blog post about running Akka.NET services using IHostedService, but with a couple of twists:

  1. When working with ASP.NET and Signalr, you often need to expose at least one of your IActorRefs to a controller or a SignalR hub and
  2. When your ActorSystem is terminated, we must also guarantee that the IWebHost shuts down - otherwise we end up with a zombie application.

We’ll show you how to handle both of those in this post.

Exposing IActorRefs to ASP.NET and SignalR

As a best practice we typically host our ActorSystem inside an IHostedService running in the background of our ASP.NET application - that way we can take advantage of Microsoft.Extensions.DependencyInjection and some of the Microsoft.Extensions.Hosting lifecycle hooks easily.

However, what happens when something inside our ASP.NET application takes a dependency on one of our actors? How do we connect those two components together?

Using our popular Cluster.WebCrawler code sample as a reference - it uses SignalR to communicate between the end-user and the ActorSystem via the CrawlHub:

public class CrawlHub : Hub
{
    private readonly ISignalRProcessor _processor;

    public CrawlHub(ISignalRProcessor processor)
    {
        _processor = processor;
    }

    public void StartCrawl(string message)
    {
        _processor.Deliver(message);
    }
}

The CrawlHub takes a dependency on a ISignalRProcessor, which forwards the string payloads to some other processor behind the scenes.

public interface ISignalRProcessor
{
    void Deliver(string rawMsg);
}

ASP.NET Core’s dependency injection will automatically pass a matching ISignalRProcessor definition, if one is defined. So where does that definition come from?

/// <summary>
/// <see cref="IHostedService"/> that runs and manages <see cref="ActorSystem"/> in background
/// </summary>
public sealed class AkkaService : IHostedService, ISignalRProcessor
{
    private ActorSystem _clusterSystem;
    private readonly IServiceProvider _serviceProvider;
    private IActorRef _signalRActor;

    private readonly IHostApplicationLifetime _applicationLifetime;

    public AkkaService(IServiceProvider serviceProvider, IHostApplicationLifetime appLifetime)
    {
        _serviceProvider = serviceProvider;
        _applicationLifetime = appLifetime;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        var config = HoconLoader.ParseConfig("web.hocon");
        var bootstrap = BootstrapSetup.Create()
            .WithConfig(config.ApplyOpsConfig()) // load HOCON and inject environment variables
            .WithActorRefProvider(ProviderSelection.Cluster.Instance); // launch Akka.Cluster

        // N.B. `WithActorRefProvider` isn't actually needed here
        // the HOCON file already specifies Akka.Cluster

        // enable DI support inside this ActorSystem, if needed
        var diSetup = ServiceProviderSetup.Create(_serviceProvider);

        // merge this setup (and any others) together into ActorSystemSetup
        var actorSystemSetup = bootstrap.And(diSetup);

        // start ActorSystem
        _clusterSystem = ActorSystem.Create("webcrawler", actorSystemSetup);

        _clusterSystem.StartPbm(); // start Petabridge.Cmd (https://cmd.petabridge.com/)

        // instantiate actors
        var router = _clusterSystem.ActorOf(Props.Empty.WithRouter(FromConfig.Instance), "tasker");
        var processor = _clusterSystem.ActorOf(
            Props.Create(() => new CommandProcessor(router)),
            "commands");
        var signalRProps = ServiceProvider.For(_clusterSystem).Props<SignalRActor>(processor);
        _signalRActor = _clusterSystem.ActorOf(signalRProps, "signalr");

        // add a continuation task that will guarantee 
        // shutdown of application if ActorSystem terminates first
        _clusterSystem.WhenTerminated.ContinueWith(tr => {
            _applicationLifetime.StopApplication();
        });

        return Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        // strictly speaking this may not be necessary - terminating the ActorSystem also works
        // but this call guarantees that the shutdown of the cluster is graceful regardless
        await CoordinatedShutdown.Get(_clusterSystem)
            .Run(CoordinatedShutdown.ClrExitReason.Instance);
    }

    public void Deliver(string rawMsg)
    {
        _signalRActor.Tell(rawMsg);
    }
}

Our AkkaService implements both IHostedService and ISignalRProcessor - and we will use this to expose our SignalRActor directly to the SignalR CrawlHub.

Next - we need to bind our dependencies inside Startup.cs:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public static IServiceProvider Provider { get; private set; }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
        services.AddSignalR();
        services.AddSingleton<CrawlHubHelper, CrawlHubHelper>();

        // creates an instance of the ISignalRProcessor that can be handled by SignalR
        services.AddSingleton<ISignalRProcessor, AkkaService>();

        // starts the IHostedService, which creates the ActorSystem and actors
        services.AddHostedService<AkkaService>(sp => 
            (AkkaService)sp.GetRequiredService<ISignalRProcessor>());
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/error");
        }

        app.UseStaticFiles();
        app.UseRouting();
        app.UseEndpoints(ep => {
            ep.MapControllerRoute("default",
                "{controller=Home}/{action=Index}/{id?}");
            ep.MapHub<CrawlHub>("/hubs/crawlHub");
        });
    }
}

The key implementation detail here is the two different calls we make to bind the AkkaService to the IServiceCollection:

// creates an instance of the ISignalRProcessor that can be handled by SignalR
services.AddSingleton<ISignalRProcessor, AkkaService>();

// starts the IHostedService, which creates the ActorSystem and actors
services.AddHostedService<AkkaService>(sp => 
    (AkkaService)sp.GetRequiredService<ISignalRProcessor>());

First, we create a singleton binding for the ISignalRProcessor which is going to own the AkkaService and its service lifetime. This binding will only ever invoke the constructor on the AkkaService - it won’t call any of the IHostedService lifetime methods.

Second, we retrieve the original singleton AkkaService binding and then start that service as an IHostedService - this will cause all of the normal IHostedService lifecycles to run.

This arrangement will work equally well for exposing IActorRefs through ASP.NET Core endpoints, if need be.

Exposing SignalR to Akka.NET Actors

In the Cluster.WebCrawler example we not only need to expose some Akka.NET actors to SignalR, but we also need to enable some of our actors to communciate directly with SignalR too! How do we go about this?

First, the tool you want to use to communicate with a SignalR hub externally is an IHubContext - that’s the right tool for the job.

So in our case, we’re going to create a small abstraction that calls some specific methods on the hub in order to hide that detail from the actor who will eventually consume it.

/// <summary>
///     Necessary for getting access to a hub and passing it along to our actors
/// </summary>
public class CrawlHubHelper
{
    private readonly IHubContext<CrawlHub> _hub;

    public CrawlHubHelper(IHubContext<CrawlHub> hub)
    {
        _hub = hub;
    }

    public void PushStatus(IStatusUpdateV1 update)
    {
        WriteMessage(
            $"[{DateTime.UtcNow}]({update.Job}) - 
                {update.Stats} ({update.Status}) [{update.Elapsed} elapsed]");
    }

    public void CrawlFailed(string reason)
    {
        WriteMessage(reason);
    }

    internal void WriteRawMessage(string msg)
    {
        WriteMessage(msg);
    }

    internal void WriteMessage(string message)
    {
        _hub.Clients.All.SendAsync("writeStatus", message);
    }
}

Next, we will use dependency injection to pass this CrawlHubHelper to our SignalRActor, who will use it:

/// <summary>
///     Actor used to wrap a signalr hub
/// </summary>
public class SignalRActor : ReceiveActor, IWithUnboundedStash
{
    private readonly IActorRef _commandProcessor;

    private readonly CrawlHubHelper _hub;

    public SignalRActor(IActorRef commandProcessor, CrawlHubHelper hub)
    {
        _commandProcessor = commandProcessor;
        _hub = hub;

        HubAvailable();
    }


    public IStash Stash { get; set; }

    private void HubAvailable()
    {
        Receive<string>(str => { _commandProcessor.Tell(new CommandProcessor.AttemptCrawl(str)); });

        Receive<CommandProcessor.BadCrawlAttempt>(bad =>
        {
            _hub.CrawlFailed($"COULD NOT CRAWL {bad.RawStr}: {bad.Message}");
        });

        Receive<IStatusUpdateV1>(status => { _hub.PushStatus(status); });

        Receive<IStartJobV1>(start =>
        {
            _hub.WriteRawMessage($"Starting crawl of {start.Job.Root}");
        });

        Receive<DebugCluster>(debug => { _hub.WriteRawMessage($"DEBUG: {debug.Message}"); });
    }

    public class DebugCluster
    {
        public DebugCluster(string message)
        {
            Message = message;
        }

        public string Message { get; }
    }
}

The CrawlHubHelper will be passed to the SignalRActor using the Akka.DependencyInjection.ServiceProvider (which we wrote about here):

var signalRProps = ServiceProvider.For(_clusterSystem).Props<SignalRActor>(processor);
_signalRActor = _clusterSystem.ActorOf(signalRProps, "signalr");

And from there, the actor can safely communicate with the CrawlHub over an indefinite period of time.

Cleanly Terminating ASP.NET Upon ActorSystem Termination

One other small wrinkle that many production Akka.NET users initially run into is a scenario, typically when using Akka.Cluster, where the ActorSystem may be terminated programmatically - such as when it’s kicked out of the cluster by one of the other nodes.

In this instance it’s important for us to ensure that the entire IHost responsible for running in the process foreground is terminated so the process can cleanly exit - this allows process supervision platforms such as Windows Services and Kubernetes to restart a process that exited unexpectedly.

The alternative - not cleanly shutting down the ASP.NET IHost when the ActorSystem terminates, results in an ISignalRProcessor instance that is still being actively called by SignalR or ASP.NET but can’t actually do it’s job because the actors responsible for doing the processing are now dead. That’s a recipe for a bad time.

Fortunately the solution to this problem is easy - we take a dependency on IHostApplicationLifetime, an abstraction that gives us a handle on the entire IHost’s lifecycle, inside the AkkaService constructor:

public sealed class AkkaService : IHostedService, ISignalRProcessor
{
    private ActorSystem _clusterSystem;
    private readonly IServiceProvider _serviceProvider;
    private IActorRef _signalRActor;

    private readonly IHostApplicationLifetime _applicationLifetime;

    public AkkaService(IServiceProvider serviceProvider, IHostApplicationLifetime appLifetime)
    {
        _serviceProvider = serviceProvider;
        _applicationLifetime = appLifetime;
    }

    // rest of class
}

In the AkkaService.StartAsync method, we tack on a continuation on to the ActorSystem.WhenTerminated property - a Task that completes only after the entire ActorSystem has been stopped:

// add a continuation task that will guarantee shutdown of application if ActorSystem terminates
_clusterSystem.WhenTerminated.ContinueWith(tr => {
    _applicationLifetime.StopApplication();
});

The IHostApplicationLifetime.StopApplication() method will signal to the IHost that it’s time to shut down, which will cause the Task returned by the IHost.RunAsync() method in our Program.cs to complete:

internal class Program
{
    private static async Task Main(string[] args)
    {
        var host = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddLogging();
                services.AddHostedService<TrackerService>();

            })
            .ConfigureLogging((hostContext, configLogging) =>
            {
                configLogging.AddConsole();

            })
            .UseConsoleLifetime()
            .Build();

        await host.RunAsync();
    }
}

And thus, that will guarantee that both our ActorSystem and our ASP.NET IHost terminate together - which is the desired behavior.

Akka.NET Project Templates

So all of this is pretty straightforward and gives us a repeatable way of hosting, configuring, and safely disposing our Akka.NET and ASP.NET services. If you want to use this type of functionality inside your own applications we’ve made a set of dotnet new template for this exact purpose.

Install

To install Petabridge’s Akka.NET templates, execute the following command using the .NET CLI:

PS> dotnet new -i "Petabridge.Templates::*"

Use

To create a brand new headless Akka.NET application that uses this IHostedService setup, just run the following dotnet new command:

PS> dotnet new pb-akka-cluster -n [app name]

This will give you a brand new project that is ready to be Dockerized and deployed as a .NET 5 headless service.

We also include a second template that will do the same, but for running Akka.NET as a background service inside an ASP.NET application:

PS> dotnet new pb-akka-web -n [app name]

Happy coding!

If you liked this post, you can share it with your followers or follow us on Twitter!
Written by Aaron Stannard on May 12, 2021

 

 

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.