Note: this post is out-of-date—Akka.NET now runs on Dotnet Core
Currently there is no DotNet Core runtime support for Akka.NET or SignalR. I was about resign myself to the idea of waiting for another year or so to try them out together in the new environment, but I randomly came across a blog that mentions that you can run non-core libraries on ASP.Net Core — if you target .Net 4.6.1. Good to know.
I’m using Visual Studio 2015 Update 3 and the DotNet Core Tools v. 1.1.
TL;DR the code for this example is at https://github.com/mikebridge/AkkaSignalR.
This demo has two moving parts. There’s a React.js front end running on Node, and a back end with the simplest Actor-based service I can think of—an “Echo Service” which just echoes back whatever it receives.
Configure a new SignalR Application
To create a C# application from scratch, create an empty solution in Visual Studio and then add an empty DotNet core Web Application project inside that.
Since we can’t target for netcoreapp1.0
yet, open
package.json
in the project and replace netcoreapp1.0
under frameworks
with net461
:
"frameworks": {
"net461": {
}
}
You’ll also need to remove these lines from dependencies
, since Microsoft.NETCore.App
is incompatible with 4.6.1:
"Microsoft.NETCore.App": { "version": "1.0.1", "type": "platform" }
Add these dependencies to package.json
(or get them from NuGet):
"Microsoft.AspNetCore.Owin": "1.1.0", "Microsoft.AspNetCore.Cors": "1.1.0", "Microsoft.AspNet.SignalR": "2.2.1", "Akka": "1.1.3", "Akka.Remote": "1.1.3"
… and execute dotnet restore
or right-click on your project’s “References” and
select “Restore Packages”.
Configure Startup.cs
Since SignalR has an old-style setup method, we’ll have to bridge the gap between the old IAppBuilder and the new
IApplicationBuilder-based configuration.
I adapted the official Microsoft
UseAppBuilder
extension function here.
This is just a temporary solution until the new pipeline is supported in SignalR.
Startup.cs
will look something like this:
public class Startup
{
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
services.AddCors(options =>
{
options.AddPolicy("default", policy =>
{
policy.WithOrigins("http://localhost:3000")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseCors(policy =>
{
policy.WithOrigins("http://localhost:3000");
policy.AllowAnyHeader();
policy.AllowAnyMethod();
policy.AllowCredentials();
});
var actorSystem = ActorSystem.Create("SignalRChatAPI");
var echoActor = actorSystem.ActorOf(Props.Create(() => new SignalREchoActor()), "echoActor");
app.UseAppBuilder(appBuilder => appBuilder.Use((ctx, next1) =>
{
// make the actor system available via the owin environment
ctx.Environment["akka.actorsystem"] = actorSystem;
return next1();
}).MapSignalR());
}
}
Note that CORS is configured to allow connections from our Node server, which will be running locally on port 3000.
Join SignalR to Akka
You can either connect Akka to SignalR via a PersistentConnection
or
using Hubs
. Either works fine, but I preferred the higher-level
abstraction of using Hubs—plus it seems easier to hook it into
authentication.
Here’s the single message type that this system supports:
using Akka.Actor;
using EchoAPI.Hub;
using EchoAPI.Messages;
using Microsoft.AspNet.SignalR;
namespace EchoAPI.Actors
{
public class SignalREchoActor : TypedActor,
IHandle<EchoRequest>
{
private IHubContext _context;
protected override void PreStart()
{
_context = GlobalHost.ConnectionManager.GetHubContext<EchoHub>();
}
public void Handle(EchoRequest message)
{
// send the message back to the client.
_context.Clients.All.echoMessage($"ECHO: {message.Message}");
}
}
}
The echo actor
handles the EchoRequest
messages it receives and sends them back to all the current JavaScript clients.
It finds those clients via the IHubContext
, which it locates using
GlobalHost.ConnectionManager.GetHubContext
:
using Akka.Actor;
using EchoAPI.Hub;
using EchoAPI.Messages;
using Microsoft.AspNet.SignalR;
namespace EchoAPI.Actors
{
public class SignalREchoActor : TypedActor,
IHandle<EchoRequest>
{
private IHubContext _context;
protected override void PreStart()
{
_context = GlobalHost.ConnectionManager.GetHubContext<EchoHub>();
}
public void Handle(EchoRequest message)
{
// send the message back to the client.
_context.Clients.All.echoMessage($"ECHO: {message.Message}");
}
}
}
The Hub passes along client requests to the Echo Actor, and it finds the actor via the ActorSystem. I’m
getting the ActorSystem from the Owin environment variable Context.Request.Environment["akka.actorsystem"]
that we configured in Startup.cs
, but the Offical Documentation says
that you can use a global variable to find the Actor instead.
using System;
using System.Diagnostics.CodeAnalysis;
using Akka.Actor;
using EchoAPI.Messages;
using Microsoft.AspNet.SignalR.Owin;
namespace EchoAPI.Hub
{
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
public class EchoHub : Microsoft.AspNet.SignalR.Hub
{
public void SendMessage(String message)
{
var actorSystem = FindActorSystem();
var echoActorRef = actorSystem.ActorSelection("/user/echoActor");
echoActorRef.Tell(new EchoRequest(message));
}
private ActorSystem FindActorSystem()
{
var ctx = Context.Request as ServerRequest;
if (ctx == null)
{
throw new Exception("The context was not initialized");
}
var actorSystem = ctx.Environment["akka.actorsystem"] as ActorSystem;
if (actorSystem == null)
{
throw new Exception("The ActorSystem was not initialized");
}
return actorSystem;
}
}
}
You will need to adjust the port that the API runs on in Program.cs.
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseUrls("http://localhost:5001") // add this to run on port 5001
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
host.Run();
}
Miscellaneous Configuration
To be able to launch the server from Visual Studio,
- Uncheck
Properties->Debug->Launch URL
- Set
Properties->Debug->WebServer Settings->App URL
tohttp://localhost:5001/
.
Also, if you want capitalized C# method names to be camelCased for the JavaScript client, you’ll need to do something like this. (I’m using this implementation).
Next Steps
Now that we have SignalR and Akka.NET working together with Owin Middleware, it will be relatively easy to add JWT Token authentication.
Also, the current React code is rather ugly. It would also be nice to have a better separation of concerns so that connecting, messaging and view handling are more modular.
I’ll add some notes on both these in upcoming posts.