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:
- When working with ASP.NET and Signalr, you often need to expose at least one of your
IActorRef
s to a controller or a SignalR hub and - When your
ActorSystem
is terminated, we must also guarantee that theIWebHost
shuts down - otherwise we end up with a zombie application.
We’ll show you how to handle both of those in this post.
Exposing IActorRef
s 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 IActorRef
s 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!
- 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.