Key Takeaways
- Based on Microsoft.AspNetCore.Sockets, SignalR Core does not depend on HTTP anymore
- Binary protocols are now supported, based on MessagePack serialization format
- TypeScript client removes 3rd party library dependencies
- Supports WebSocket native clients, you can build your own clients to connect to a SignalR server
- Scale-out is extensible and flexible, now it's possible to implement your own ways to scale out
Some months ago when the SignalR Core team released an unofficial version of the new Asp.Net Core SignalR it provided an opportunity could get to know how it works and exposed the differences between SignalR for ASP.NET and the new architecture for SignalR Core.
SignalR Core – What’s Been Removed
In comparing the two versions of SignalR it soon became apparent that some important features that are not supported anymore. The first omission is jQuery and 3rd party library dependencies, since the new JavaScript client is made with Typescript. Auto-reconnect with message replay is next, and the main reason for its removal is due to performance issues. The problem was that the server had to keep a buffer per connection in order to store messages for retransmission. If several clients found themselves disconnected at once, it could be overwhelming and this way it can tries re-send it again to the client when the connection is restored, so you can imagine how the server works when there are a lot of clients and these clients lost a lot of messages. Another feature that SignalR team got rid is multi-hub endpoints. The result is that there is now a single hub per connection.
The Scale out model is an important feature that is no longer supported by SignalR Core. It has been removed because the MessageBus
was used as a “golden hammer” to scale out, and it only supported the following approaches: Azure Service Bus, Redis and SQL Server. The problem with using the MessageBus this way is that in collaborative scenarios (client-to-client), those 3 ways to scale out could become in a bottle neck as the number of messages grows with the number of clients.
However, I think it was a bit radical decision cease the scale-out model because the MessageBus worked fine in some scenarios. For instance, when we’re using SignalR as a server broadcaster the server has the control of the quantity of messages what are sent. Now in SignalR Core Alpha it provides the option to scale out in order according to the needs of its developer, depending business needs, system constraints or infrastructure hosting the software. The resulting design is more “plug and play”. The SignalR Core team has provided an example of how SignalR Core can scale out using Redis. Other providers are being considered for inclusion in the final release of SignalR Core.
The last feature to be removed is multi-server ping-pong (backplane), because it generated a lot of traffic in the server farm. ASP.NET SignalR replicates every message over all servers through the MessageBus since the client could be connected to any server into the farm, now in SignalR Core sticky sessions are used to avoid replicating the messages around all servers. A benefit of this approach is that SignalR Core now knows what client is connected to what server.
SignalR Core – What’s Been Added
Now lets look at the new features SignalR Core brings to the table. The first is the use of the Binary protocol to send and receive messages. In SignalR for ASP.NET, you could only send and receive messages in the text-based JSON format, now with SignalR Core it is possible to also send and receive messages in binary. This new binary protocol is based on MessagePack serialization format which is faster and smaller than JSON.
Host-agnostic is another important feature because it allows to get rid of HTTP dependency since SignalR Core connections are agnostics, for instance, now we can use SignalR over HTTP or TCP. EndPoints API is a new characteristic as well and maybe the most important one since it’s the building block of this new version and it allows to support the Host-Agnostic feature, that is possible because of this new version is based on Microsoft.AspNetCore.Sockets which is a low-level networking abstraction thus it works with sockets directly, so in the end a SignalR Hub is an EndPoint as well.
The Multiple formats feature is a cool one because it allows you to handle any kind of format to send and receive messages. It allows us to have different clients to talk in different languages (formats) but connected to the same endpoint, that means SignalR Core is now format agnostic. In this example, you can see that there are three types of formats (JSON, PIPE and Protobuf) for one single endpoint, what are resolved at runtime both to read and to write, in this case, thanks to a custom format resolver. This is possible because as noted earlier, an EndPoint relies on Microsoft.AspNetCore.Sockets thus at the end of the day, it is simply transporting bytes.
This new version also Supports WebSocket native clients, so you're not bound to use the SignalR web client if you don’t want too. Previously with ASP.NET SignalR developers had to use the JavaScript-based web client in order to connect with a SignalR server. With SignalR Core we can build our own clients if we prefer, taking advantage of the browser APIs to do this in a native way. But as I said earlier if we want we can also take advantage of the new TypeScript Client, which lets us benefit from all the features that TypeScript offers. What’s more, this client is published through the NPM package manager which makes handling dependencies much easier.
The last one is that Scale out is extensible and flexible, and as I said earlier in this version the SignalR Core team improved and simplify the scale-out model and they’re providing a Redis based scale-out component example in order we can get to know how we can perform our ways to scale out.
Last September 14th the SignalR Core team announced an Alpha release and later on October 9th they announced Alpha2 which is the official preview release to SignalR Core 2.0. Today we’re going to talk about the changes included in this version which is the most recent “stable” and official version of SignalR Core.
When I got to know about the new version immediately I went to my repo and to try and build this latest code. As it can be expected with code under development, it didn’t build immediately. Despite this initial obstacle it’s very promising because you can witness the evolution and improvements firsthand, which helps to realize why the changes happened. Now I’m going to walk you through the challenges I faced, why they didn’t work and what I did to fix them.
In order to use SignalR Core in your projects, you must reference the Microsoft.AspNetCore.SignalR which latest version is 1.0.0-alpha2-final |
HubConnectionBuilder
In the previous version when we needed to connect with any Hub from the server side, we just used the HubConnection
class, just like this:
var connection = new HubConnection(new Uri(baseUrl), loggerFactory);
This was the first change that broke my application’s build, as now we have to use the HubConnectionBuilder
class (it implements Builder pattern) to make the connection with any SignalR Core Hub. This change makes the connection creation more extensible and avoids using a constructor full of parameters or unnecessary null parameters. It is a change that I like as it simplifies for the developer connection creation. The change also benefits SignalR Core internally HubConnection
instantiation is performed as it improves the maintainability (from SignalR Core team point of view) and the simplicity (from the final developer point of view):
var connection = new HubConnectionBuilder()
.WithUrl(baseUrl)
.WithConsoleLogger()
.Build();
Connection server-side handlers
Previously when you had a delegate method in your client (in this case a server-side client) to handle data when SignalR Hub broadcasts to the clients you used the On
method. You then needed to specify into an array the parameters that the method received:
connection.On("UpdateCatalog", new[] { typeof(IEnumerable<Product>) }, data =>
{
var products = data[0] as List<Product>;
foreach (var item in products)
{
Console.WriteLine($"{item.name}: {item.quantity}");
}
});
As you can see, this method was a bit uncomfortable to use because you always needed to specify an array with the types that the method received and even if the method did not need any parameters you still needed to specify an empty array. A big problem with this design was that the handler parameters are typeless, so even though you already had specified the types into the array you needed to search into the array of parameters and cast the objects to the appropriate type.
With this latest release of SignalR Core, there are new generic method overloads and we can specify the parameter’s type in a safe and easy way to avoid casting objects. These new generic methods are just extensions methods that wrap the original one to make life easier for developers. The resulting code looks nicer, simpler and is much more readable.
connection.On<List<Product>>("UpdateCatalog", products =>
{
// now, “products” parameter is a List<Product> type.
foreach (var item in products)
{
Console.WriteLine($"{item.name}: {item.quantity}");
}
});
Naming convention
I noticed (because it broke my application’s build as well) a name changes in the method Invoke
:
await connection.Invoke("RegisterProduct", cts.Token, product, quanity);
As you can see it’s an async method, so now following a naming convention is called InvokeAsync
, and also the parameters order is changed, the cancellation token in this overload it’s the last one:
await connection.InvokeAsync("RegisterProduct", product, quanity, cts.Token);
The important point here is that by following conventions and standards it is a much easier and intuitive interaction for developers using SignalR Core API (including the SignalR Core developers team) as it provides uniformity to the code. The resulting code is speaks for itself. For instance, in this case when a developer inspects this method in their editor using Intellisense, they will realize in advance that this method works asynchronously.
Another change related to naming convention was about on the MapEndpoint
method, now it’s called MapEndPoint
. As you can see it applies the Pascal case style.
Before:
app.UseSockets(routes =>
{
routes.MapEndpoint<MessagesEndPoint>("/message");
});
Now:
app.UseSockets(routes =>
{
routes.MapEndPoint<MessagesEndPoint>("message");
});
If you see, now you don’t need the “/” on the beginning, is the same with MapHub
method. Actually, that’s an issue that we (a member of SignalR Core team and I) realized was happening. It occurred because those methods weren’t using the PathString
API, but for the next version it will work normally again, with the “/” just like others .Net Core API’s. (I made this contribution myself to SignalR Core, and you can make contribute your changes to the project too thanks to its public development process.)
Naming changes
There were a couple of naming changes there, one of them over the Connection
class, now it’s called ConnectionContext
, because it’s more than just a connection. A ConnectionContext has other members related to the connection, including metadata, channels, etc.
Before:
public override async Task OnConnectedAsync(Connection connection)
Now:
public override async Task OnConnectedAsync(ConnectionContext connection)
The other naming change was about on the Transport
object into ConnectionContext
class. Before the properties to manage the Input and Output were called just Input
and Output
respectively, now they are called In
and Out
.
Before:
connection.Transport.Input.WaitToReadAsync()
connection.Transport.Output.WriteAsync()
Now:
connection.Transport.In.WaitToReadAsync()
connection.Transport.Out.WriteAsync()
TryRead and WriteAsync
TryRead
and WriteAsync
methods were simplified, as before they received a Message object like parameter.
Before:
Message message;
if (connection.Transport.Input.TryRead(out message))
{
...
}
connection.Transport.Output.WriteAsync(new Message(payload, format, endOfMessage));
Now:
// message is byte[]
if (connection.Transport.In.TryRead(out var message))
{
...
}
// payload is byte[]
connection.Transport.Out.WriteAsync(payload);
Now they use an array of bytes for their parameter. The reason for this change is because Channel<byte[]>
is used by the Sockets layer, which is a low-level networking abstraction designed to behave like a standard TCP socket. The SignalR Core team feels that moving this data up the SignalR stack to keep Sockets layer cleaner. Previously the SignalR Core team used a low-level framing protocol on top of that raw socket which included this data. (This was not done for WebSockets, because it already has framing.)
The result is that the Microsoft.AspNetCore.Sockets layer has been “decontaminated”, allowing EndPoints to work only with raw binary data. This lets the EndPoint apply whatever protocol it wants, such as TCP or HTTP.
Implementation of the the low-level framing protocol for Hubs is done in the Microsoft.AspNetCore.SignalR layer, so Message Types, Framing, etc. are all handled in the IHubProtocol implementations now, such as JsonHubProtocol and MessagePackHubProtocol. This design provides an extensible way to implement others hub protocols.
Other changes
We can take advantage of the signalr-client
module by installing it directly from npm. This way we don’t need to have a static file referenced into the website and tied to the source control. Here I added the signalr-client
as a client-side dependency into the package.json
file:
{
"version": "1.0.0",
"name": "asp.net",
"private": true,
"dependencies": {
"@aspnet/signalr-client": "^1.0.0-alpha2-final",
"jQuery.tabulator": "^1.12.0"
}
}
Visual Studio will automatically install the packages when you build the solution. Also, we can take advantage of the new bundling feature built-in to .NET Core. This bundling will copy the signalr-client files to the wwwroot folder automatically. This way you don’t need dealing with gulp, grunt or another task runner.
[
{
"outputFileName": "wwwroot/lib/signalr/signalr-clientES5-1.0.0-alpha2-final.min.js",
"inputFiles": [
"node_modules/@aspnet/signalr-client/dist/browser/signalr-clientES5-1.0.0-alpha2-final.min.js"
],
"minify": {
"enabled": false
}
},
{
"outputFileName": "wwwroot/lib/signalr/signalr-client-1.0.0-alpha2-final.min.js",
"inputFiles": [
"node_modules/@aspnet/signalr-client/dist/browser/signalr-client-1.0.0-alpha2-final.min.js"
],
"minify": {
"enabled": false
}
}
]
I faced a stumbling block here because by default .NET Core bundling enables minify and I was referencing these files that was already minified, so it was trying to minify something minified and the build got an error, so I had to disable the minify option. |
Conclusion
Those were the changes that I faced after my upgrade to this new SignalR core version. I wanted to share them so that developers can understand what the new changes were and why they were made. I hope it will be helpful and I encourage you to test this new SignalR Core version with your own projects.
Fixing the build errors due to the changes described in this version of SignalR Core took me just a couple of hours. Diving into the code and understanding the new changes added a few more hours to this total but the results have been worth the effort.
About the Author
Geovanny Alzate Sandoval is a System Engineer from Medellín, Colombia, and enjoys everything related to software development, new technologies, design patterns, and software architecture. He worked for more than 10 years in this passionate world where having the opportunity to work as a developer, technical leader, and software architect. He loves contributing to the community and write on his blog about new stuff related with Microsoft technologies. In addition, he's also a co-organizer of MDE.NET community, which is a community for .NET developers in Medellín.