

Best Practices for Integrating Akka.NET with ASP.NET Core and SignalR
source link: https://petabridge.com/blog/akkadotnet-aspnetcore/
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.

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::*"
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!Upcoming Petabridge Live Akka.NET Webinar Trainings
Get up to speed on the leading edge of large-scale .NET development with the Petabridge team. Each training is done remotely via webinar, lasts four hours, and will save you weeks of trial and error.
Get to the cutting edge with Akka.NET
Learn production best practices, operations and deployment approaches for using Akka.NET.
Recommend
-
170
https://weblogs.asp.net/ricardoperes/signalr-in-asp-net-core作者:Ricardo Peres译者:oopsguy.com 介绍SignalR 是一个用于实现实时网站的 Microsoft .NET 库。它使用多种技术来实现服务器与客户端间的双向通信,服务器可以随时将消息推送到连接的客户端。
-
65
SignalR Core尝鲜
-
39
This is the twelfth of a newseries of postson ASP .NET Core. In this post, we’ll learn about the use of SignalR to build real-time functionality in your ASP NET Core web apps. SignalR can also be used to add real-time fu...
-
16
参考文章:微软官方文档:https://docs.microsoft.com/zh-cn/aspnet/core/signalr/introduction?view=aspnetcore-3.1 和 https://www.cnblogs.com/cgzl/p/9509207.html 系列 ASP.NET Core SignalR 简介 什么是 SignalR? ASP.NET Core...
-
13
In this blog post we’re going to cover some best practices you can use when designing domain events and objects intended to work with Akka.NET. If you follow these best practices you’ll run into fewer errors, clearer log messages, and a bette...
-
29
Dependency Injection Best Practices for Akka.NET550 views•Jan 20, 2021
-
9
SignalR vs gRPC on ASP.NET Core – which one to choose My book, SignalR on .NET 6 – the complete guide, is out now! Also available
-
6
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:
-
7
Best practices for integrating content design in your design systemHow to enrich your design system with content design
-
2
Best Practices I Wish We Knew When Integrating Stripe Webhooks January 11th 2023 New Story7 min by
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK