A lot is demanded from Web applications today. Web apps are expected not only to function properly, but they need to do so with a great user experience. Users expect applications to be fast. They also expect applications to deliver information in real-time, without the need to refresh the browser.
Find additional: .NET Core articles
All major programming platforms have frameworks that make it easier to build real-time applications. The most well-known real-time framework is probably Socket.IO on Node.js. ASP.NET has a popular real-time framework called SignalR.
In this article, you'll learn about the origins of SignalR and how it has been rewritten to run on ASP.NET Core and address the needs of today's real-time applications. You'll also learn how to get started building ASP.NET Core SignalR applications.
The History of SignalR
SignalR was created in 2011 by David Fowler and Damian Edwards, who now play key roles in the direction and development of ASP.NET. It was brought into the ASP.NET project and released as part of ASP.NET in 2013. At the time, the WebSocket protocol had just been standardized and most browsers didn't support it.
To achieve real-time messaging for the Web, developers used inefficient techniques such as AJAX polling and long-polling, and technologies like server-sent events that weren't broadly implemented by browsers. SignalR was set up to solve this problem and provide easy support for real-time capabilities on the ASP.NET stack by creating server- and client-side libraries that abstract away the complications of these technologies.
Behind the scenes, SignalR negotiates the best protocol to use for a specified connection based on what's supported by both the server and the client. It then provides a consistent API for sending and receiving messages in real-time. Because it's so easy to use, ASP.NET developers quickly adopted SignalR, making it the de facto stack for real-time ASP.NET development.
A lot can change on the Web in five years. When SignalR was created, almost every Web application used jQuery, so it was an easy decision for the SignalR JavaScript client to depend on jQuery as well. SignalR also contained complex logic focused on managing the different protocols and workarounds to achieve real-time messaging on the Web before the WebSocket protocol was widely adopted by browsers.
Fast forward to 2018. A lot of jQuery's functionality can be achieved with plain JavaScript, and full-fletched front-end frameworks such as Angular, React, and Vue have emerged to replace jQuery as the new foundations on which single-page applications (SPAs) are built. WebSockets are available in all major browsers and are now the standard for real-time Web communications.
Other features in SignalR were intended to make it easier to use, such as automatic reconnection and turnkey scale-out, but they ended up adding complexity and inefficiencies to the framework.
So when ASP.NET was reimagined from the ground up to create a faster, cross-platform ASP.NET Core, the team also took the time to rewrite SignalR from scratch, taking into account all that was learned from the first two versions of SignalR, as well as making it extensible enough to future-proof against new protocols and transport technologies that may emerge.
Introducing ASP.NET Core SignalR
SignalR on .NET Core runs on ASP.NET Core 2.1, which can be downloaded at http://aka.ms/DotNetCore21.
Overall, ASP.NET Core SignalR maintains a lot of the same core concepts and capabilities as SignalR. Hubs continue to be the main connection point between the server and its clients. Clients can invoke methods on the hub, and the hub can invoke methods on the clients. The hub has control over the connections on which to invoke a certain method. For example, it can send a message to a single connection, to all connections belonging to a single user, or to connections that have been placed in arbitrary groups.
There were several noteworthy changes between ASP.NET SignalR and ASP.NET Core SignalR; let's go over some of them right now.
JavaScript Client Library
In the browser, the biggest change is the removal of the jQuery dependency. The JavaScript/TypeScript client library can now be used without referencing jQuery, allowing it to be used with frameworks such as Angular, React, and Vue without friction. In addition, this allows the client to be used in a Node.js application.
To align with the expectations of the front-end development community, the JavaScript client is now acquired through npm. It's also hosted on Content Delivery Networks (CDNs).
In addition to the JavaScript/TypeScript client library, ASP.NET Core SignalR also ships with a .NET client NuGet package. SignalR had clients for other languages such as Java, Python, Go, and PHP, all created by Microsoft and the open source community; you can expect the same for ASP.NET Core SignalR as its adoption increases.
Built-in and Custom Protocols
ASP.NET Core SignalR ships with a new JSON message protocol that's incompatible with earlier versions of SignalR. In addition, it has a second built-in protocol based on MessagePack, which is a binary protocol that has smaller payloads than the text-based JSON.
If you want to implement a custom message protocol, ASP.NET Core SignalR has extensibility points that allow new protocols to be plugged in.
Dependency Injection
ASP.NET didn't have dependency injection built in, so SignalR provided a GlobalHost class that included its own dependency resolver. Now that ASP.NET Core ships with an inversion of control (IoC) container, ASP.NET Core SignalR simply leverages the built-in framework for dependency injection.
Hubs in ASP.NET Core SignalR now support constructor dependency injection without extra configuration, just like ASP.NET Core controllers or razor pages do. It's also easy to gain access to a hub's context from outside the hub itself by retrieving an IHubContext
SignalR shipped with built-in support for scale-out using Redis, Service Bus, or SQL Server as a backplane. A backplane allows different instances of the same ASP.NET SignalR application to communicate with one another to broadcast messages to the correct clients, regardless of which instance the clients are connected to. This proved to be difficult to implement correctly and added a lot of overhead; it also didn't consider that different applications have different scale-out needs. The result was a scale-out functionality that was complex, inefficient, and didn't work well for many scenarios. ASP.NET Core SignalR was redesigned with a simpler and more extensible scale-out model. It no longer allows a single client to connect to different server-side instances between requests. This means that sticky sessions are required to ensure server affinity for clients using protocols other than WebSockets. ASP.NET Core SignalR currently provides a scale-out plug-in for Redis. Later in this article, you'll also learn about a new, fully managed Azure SignalR Service that allows you to massively scale out your ASP.NET Core SignalR applications with only minor code changes. Azure SignalR Service also enables non-.NET and serverless applications to provide real-time messaging to SignalR compatible clients. Another design decision that seemed like a good idea when SignalR first came out was automatic reconnections. SignalR included reconnection logic on both the clients and the server. Clients attempted to reconnect if a connection was lost, and the server buffered unsent messages and replayed them when a client reconnected. This also proved to be buggy and inefficient, and the implementation didn't make sense for all applications. ASP.NET Core SignalR doesn't support automatic reconnection or automatic buffering of messages. Instead, it's up to the client application to decide when it needs to reconnect; and it's up to the server to implement message buffering if required. Let's create a simple chat application to demonstrate how to use ASP.NET Core SignalR. Like typical ASP.NET Core development, you can use the dotnet command line interface (.NET Core SDK 2.0 and later) and an editor such as Visual Studio Code, Visual Studio 2017, or Visual Studio for Mac to build ASP.NET Core SignalR applications. This article builds on a new ASP.NET Core Razor Pages application with individual authentication. You can create one in Visual Studio's new ASP.NET Core project dialog or run the following .NET CLI command: A new ASP.NET Core Razor Pages application with individual authentication is created. By default, users are stored in a SQLite database. To use ASP.NET Core SignalR, it must be added to the project from NuGet. The latest version at the time of writing this is RC1. A hub is the central point in an ASP.NET Core application through which all SignalR communication is routed. Create a hub for your chat application by adding a class named Chat that inherits from Microsoft.AspNetCore.SignalR.Hub: The SendMessage method is invoked by the clients whenever a message needs to be sent. It uses the For the hub to function, SignalR needs to be enabled in the application. To do this, make a few changes to In Now it's time to add some client-side code that will interact with the Chat hub. In Pages/Index.cshtml, reference the SignalR browser JavaScript library. This can be done by using npm and a tool like WebPack to install the package and copy the client-side JavaScript files to the wwwroot folder. You can also reference the script on a CDN (note that the original URL has single @ signs, but @ is a special character in Razor and is escaped with The final step is to add some JavaScript to build and start a HubConnection (see Listing 1). Add a function to execute when newMessage is invoked. Also add some code to invoke SendMessage on the server to send a new chat message. If you run the application and open it up on two or more browsers, the simple chat application should be functional, as shown in Figure 1. SignalR authentication and authorization use the same claims-based identity infrastructure provided by ASP.NET Core. Just like authorization in ASP.NET Core, you can use the AuthorizeAttribute to require authorization to access a SignalR hub or a SignalR hub's methods. Also modify the SendAsync call to use the logged in username. By default, ASP.NET Core identity uses cookies for authentication. When a Web client connects to a SignalR hub, any existing authentication cookies are sent in the request headers. To access a hub that has authorization enabled, you'll need to log into your application before connecting to it. Now you'll see the authenticated user's username, as shown in Figure 2. For ASP.NET Core SignalR to support authentication for non-browser-based clients, you need to implement an alternative authentication mechanism that doesn't rely on cookies. A common way to include a client's identity on AJAX requests is via bearer tokens in an Authorization header. Unfortunately, you cannot set the Authorization header on WebSocket requests using JavaScript in the browser. To get around this limitation, the SignalR client library supports passing the token in a query string value named A common way to include a client's identity on AJAX requests is via bearer tokens in an Authorization header. To accept the token from the query string, configure ASP.NET Core's authentication middleware in the ConfigureServices method in Startup.cs to set the user identity on the request using a JSON Web token (JWT) if it's available in the query string (Listing 2). Next, create an ASP.NET Core controller named TokenController and add an action to exchange an identity cookie for a token, as shown in Listing 3. Next, change the AuthorizeAttribute on the hub to use the JWT bearer authentication scheme. Cookie authentication will no longer work on the hub; from now on, you need to supply a valid JWT when connecting to the hub. Last, change the client-side JavaScript to request a token from this endpoint and use it to connect to the SignalR hub. The HubConnection can be configured with an access token factory to include a token when creating the connection. The application continues to work, but if you inspect the requests on the network, you'll see that it's requesting a token and appending it to the SignalR hub negotiation and subsequent WebSocket connection requests (Figure 3). That works great for browsers, but what about clients that don't work well with cookies? You can add another endpoint on the ASP.NET Core application for non-Web clients to exchange a user's valid username and password for a token. Add the following action to the TokenController in the ASP.NET Core application to validate the credentials and return a JWT. So far, you've seen how to use ASP.NET Core with the JavaScript SignalR client library in the browser. SignalR also has a .NET Standard 2.0 client library that can be used to connect to a SignalR hub from applications built on .NET Core, .NET Framework, and more. To use the SignalR client library, import the Microsoft.AspNetCore.SignalR.Client package from NuGet. The following console application uses the HubConnectionBuilder from the SignalR client library to configure the hub connection. The syntax is similar to the JavaScript client. You can add methods that are invoked by the hub. Listing 4 is a console application that uses the library to connect to the SignalR hub you built earlier. When run, the application prompts for a username and password, and then uses the credentials to request a token from the token endpoint you created previously. It then connects to the SignalR hub and displays any messages that are sent. Send messages by typing into the console, as shown in Figure 4. ASP.NET SignalR automatically handles restarting the connection when a disconnection occurs. Reconnection behavior is often specific to each application. For this reason, ASP.NET Core SignalR doesn't provide a default automatic reconnection mechanism. For most common scenarios, a client only needs to reconnect to the hub when a connection is lost or a connection attempt fails. To do this, the application can listen for these events and call the start method on the hub connection. Here's how it looks in JavaScript, adding reconnection logic when a connection is closed or a connection attempt has failed. A common requirement for real-time applications is to track which users are currently online. ASP.NET Core SignalR makes it easy to add presence to an application. SignalR makes it easy to add presence to an application. A basic way to add presence to an ASP.NET Core SignalR is to track users in memory. Because a single user identity can potentially have more than one connection to the hub, the application needs to track the number of connections per user. Listing 5 shows a simple class that implements a user tracker. Whenever a connection is opened or closed, ConnectionOpened or ConnectionClosed is called. Based on the number of connections for the user associated with the connection event, the methods return a status to indicate if user has joined or left. In the hub, call the tracker in Listing 6 and broadcast a message when a user has joined or left. Also, send the list of currently online users when a connection is created, as shown in Listing 6. Now when users join or leave the chat application, a system message appears, as shown in Figure 5. This example gets you started in creating a presence system for your SignalR hub, but a more robust solution will be required to handle scale-out and server restarts. In order for ASP.NET Core applications running SignalR to scale out to more than one instance, a backplane must be set up. Instances communicate over the backplane to ensure that messages reach the correct destinations no matter which clients are connected to which instances. Backplanes can be built using technologies with publish-subscribe features, such as Azure Service Bus, Redis, or SQL Server. However, a much simpler way to scale out your ASP.NET Core SignalR application is to use a new Azure service called Azure SignalR Service. SignalR Service handles the scale-out for you, so you can support large numbers of connections without setting up a backplane yourself. SignalR Service integrates into an existing ASP.NET Core SignalR application using a NuGet package and requires minimal modifications to your code. To get started, use the Azure Portal to create a new SignalR Service instance, as shown in Figure 6. After the service instance is created, install the SignalR Service library into your ASP.NET Core SignalR application using the Microsoft.Azure.SignalR NuGet package. By default, the library looks for the SignalR Service connection string in an application setting named To enable Azure SignalR Service in your application, add a call to Then replace the call to UseSignalR with a call to UseAzureSignalR. Now when you run the application, instead of directly connecting to the hub in your ASP.NET Core application, clients connect to SignalR Service. All communication between clients and your application's SignalR hub go through the SignalR Service. This allows you to scale the service up and down at any time to handle different levels of traffic without any modifications to the application's code or hosting environment. If you inspect the HTTP requests in your browser (Figure 7), you can see that the application now connects to Azure SignalR Service instead of to the SignalR hub in your Web application. Note that the ASP.NET Core JWT authorization added earlier must be disabled in order for SignalR Service to integrate with this application (cookie authorization is fine). SignalR Service supports JWT authorization as well, but its integration is beyond the scope of this article. As you can see, adding real-time Web functionalities to your cross-platform Web applications is easy using ASP.NET Core SignalR. And with the Azure SignalR Service, you now have a fully managed backplane for highly scalable applications.Scale-out
Reconnections
Get Started with ASP.NET Core SignalR
Create the Initial Application
dotnet new razor --auth Individual
dotnet add package Microsoft.AspNetCore.SignalR --version 1.0.0-rc1-final
Add a SignalR Hub
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
namespace SignalRChat.Hubs
{
public class Chat : Hub
{
public async Task SendMessage(string message)
{
await Clients.All.SendAsync("newMessage", "anonymous", message);
}
}
}
Client.All
property of the hub to invoke a method named newMessage on all connected clients with arguments for the sender's username (currently “anonymous”) and the message. You'll implement the client-side SignalR code a bit later.Startup.cs
.ConfigureServices
, call the AddSignalR
extension method to configure the IoC container with services required by SignalR. Like this:public void ConfigureServices (IServiceCollection services)
{
// ...
services.AddSignalR();
}
public void Configure(IApplicationBuilder app, HostingEnvironment env)
{
// ...
app.UseAuthentication();
app.UseMvc();
app.UseSignalR (builder =>
{
builder.MapHub<Chat>("/chat");
});
}
@@
):<script src="https://unpkg.com/@@aspnet/signalr@@1.0.0-rc1-final/dist/browser/signalr.js"></script>
<div class="signalr-demo">
<form id="message-form">
<input type="text" id="message-box"/>
</form>
<hr />
<ul id="messages"></ul>
</div>
Listing 1: Build and start a HubConnection with JavaScript
<script>
const messageForm = document.getElementById('message-form');
const messageBox = document.getElementById('message-box');
const messages = document.getElementById('messages');
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chat")
.configureLogging(signalR.LogLevel.Information)
.build();
connection.on('newMessage', (sender, messageText) => {
console.log(`${sender}:${messageText}`);
const newMessage = document.createElement('li');
newMessage.appendChild(document.createTextNode(`${sender}:${messageText}`));
messages.appendChild(newMessage);
});
connection.start()
.then(() => console.log('connected!'))
.catch(console.error);
messageForm.addEventListener('submit', ev => {
ev.preventDefault();
const message = messageBox.value;
connection.invoke('SendMessage', message);
messageBox.value = '';
});
</script>
Add Authentication and Authorization
[Authorize]
public class Chat : Hub
{
public async Task SendMessage(string message)
{
await Clients.All.SendAsync("newMessage", Context.User.Identity.Name, message);
}
}
Add JSON Web Token Security
access_token
.
Listing 2: Accept the token from the query string
var key = new SymmetricSecurityKey(System.Text.Encoding.ASCII.GetBytes(Configuration["JwtKey"]));
services.AddAuthentication().AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
LifetimeValidator = (before, expires, token, parameters) =>
expires > DateTime.UtcNow,
ValidateAudience = false,
ValidateIssuer = false,
ValidateActor = false,
ValidateLifetime = true,
IssuerSigningKey = key,
NameClaimType = ClaimTypes.NameIdentifier
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(accessToken))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
services.AddAuthorization(options =>
{
options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy =>
{
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
policy.RequireClaim(ClaimTypes.NameIdentifier);
});
});
Listing 3: Add an action to exchange an identity cookie for a token
//add an action to exchange an identity cookie for a token
using System.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using SignalRChat.Data;
namespace SignalRChatDemo.Controllers
{
public class TokenController : Controller
{
private readonly SignInManager<ApplicationUser> signInManager;
private readonly IConfiguration config;
public TokenController(SignInManager<ApplicationUser> signInManager, IConfiguration config)
{
this.signInManager = signInManager;
this.config = config;
}
[HttpGet("api/token")]
[Authorize]
public IActionResult GetToken()
{
return Ok(GenerateToken(User.Identity.Name));
}
private string GenerateToken(string userId)
{
var key = new SymmetricSecurityKey(System.Text.Encoding.ASCII.GetBytes(config["JwtKey"]));
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, userId)
};
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken("signalrdemo", "signalrdemo", claims, expires: DateTime.UtcNow.AddDays(1), signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
public class LoginRequest
{
public string Username { get; set; }
public string Password { get; set; }
}
}
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class Chat : Hub
{
// ...
}
const options = {
accessTokenFactory: getToken
};
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chat", options)
.configureLogging(signalR.LogLevel.Information)
.build();
// ...
function getToken() {
const xhr = new XMLHttpRequest();
return new Promise ((resolve, reject) => {
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
if (this.status == 200) {
resolve(this.responseText);
} else {
reject(this.statusText);
}
};
xhr.open("GET", "/api/token");
xhr.send();
});
}
[HttpPost("api/token")]
public async Task<IActionResult> GetTokenForCredentialsAsync([FromBody] LoginRequest login)
{
var result = await signInManager.PasswordSignInAsync(login.Username, login.Password, false, true);
return result.Succeeded ? (IActionResult) Ok(GenerateToken(login.Username)) : Unauthorized();
}
A .NET Standard 2.0 Client for SignalR
dotnet add package Microsoft.AspNetCore.SignalR.Client --version 1.0.0-rc1-final
Listing 4: A console application that uses the library to connect
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.SignalR.Client;
using Newtonsoft.Json;
namespace SignalRChatClient
{
class Program
{
static readonly HttpClient httpClient = new HttpClient();
static readonly string baseUrl = "http://localhost:5000";
static async Task Main(string[] args)
{
Console.Write("Username: ");
var username = Console.ReadLine();
Console.Write("Password: ");
var password = "";
while(true)
{
var key = Console.ReadKey(intercept: true);
if (key.Key == ConsoleKey.Enter) break;
password += key.KeyChar;
}
var hubConnection = new HubConnectionBuilder().WithUrl($"{baseUrl}/chat", options =>
{
options.AccessTokenProvider = async () =>
{
var stringData = JsonConvert.SerializeObject(new {username, password});
var content = new StringContent(stringData);
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var response = await httpClient.PostAsync($"{baseUrl}/api/token", content);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
};
}).Build();
hubConnection.On<string, string>("newMessage", (sender, message) => Console.WriteLine($"{sender}: {message}"));
await hubConnection.StartAsync();
System.Console.WriteLine("\nConnected!");
while(true)
{
var message = Console.ReadLine();
await hubConnection.SendAsync(SendMessage", message);
}
}
}
}
Reconnections
connection.onclose(reconnect);
startConnection();
function startConnection() {
console.log('connecting...');
connection.start()
.then(() => console.log('connected!'))
.catch(reconnect);
}
function reconnect() {
console.log('reconnecting...');
setTimeout(startConnection, 2000);
}
Presence
Listing 5: The tracker class implementation
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SignalRChat
{
public class PresenceTracker
{
private static readonly Dictionary<string, int> onlineUsers = new Dictionary<string, int>();
public Task<ConnectionOpenedResult> ConnectionOpened(string userId)
{
var joined = false;
lock (onlineUsers)
{
if (onlineUsers.ContainsKey(userId))
{
onlineUsers[userId] += 1;
}
else
{
onlineUsers.Add(userId, 1);
joined = true;
}
}
return Task.FromResult(new ConnectionOpenedResult { UserJoined = joined });
}
public Task<ConnectionClosedResult> ConnectionClosed(string userId)
{
var left = false;
lock (onlineUsers)
{
if (onlineUsers.ContainsKey(userId))
{
onlineUsers[userId] -= 1;
if (onlineUsers[userId] <= 0)
{
onlineUsers.Remove(userId);
left = true;
}
}
}
return Task.FromResult(new ConnectionClosedResult { UserLeft = left });
}
public Task<string[]> GetOnlineUsers()
{
lock(onlineUsers)
{
return Task.FromResult(onlineUsers.Keys.ToArray());
}
}
}
public class ConnectionOpenedResult
{
public bool UserJoined { get; set; }
}
public class ConnectionClosedResult
{
public bool UserLeft { get; set; }
}
}
Listing 6: The tracker shows who's come and gone
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class Chat : Hub
{
private readonly PresenceTracker presenceTracker;
public Chat(PresenceTracker presenceTracker)
{
this.presenceTracker = presenceTracker;
}
public override async Task OnConnectedAsync()
{
var result = await presenceTracker.ConnectionOpened(Context.User.Identity.Name);
if (result.UserJoined)
{
await Clients.All.SendAsync("newMessage", "system", $"{Context.User.Identity.Name} joined");
}
var currentUsers = await presenceTracker.GetOnlineUsers();
await Clients.Caller.SendAsync("newMessage", "system", $"Currently online:\n{string.Join("\n", currentUsers)}");
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
var result = await presenceTracker.ConnectionClosed(Context.User.Identity.Name);
if (result.UserLeft)
{
await Clients.All.SendAsync("newMessage", "system", $"{Context.User.Identity.Name} left");
}
await base.OnDisconnectedAsync(exception);
}
public async Task SendMessage(string message)
{
await Clients.All.SendAsync("newMessage", Context.User.Identity.Name, message);
}
}
Azure SignalR Service
Azure:SignalR:ConnectionString
.AddAzureSignalR()
to the SignalR configuration in Startup.cs
.services.AddSignalR().AddAzureSignalR();
app.UseAzureSignalR(builder =>
{
builder.MapHub<Chat>("/chat");
});
Conclusion