How to Build Real Time Applications with Asp.net Core SignalR - Windows ASP.NET Core Hosting 2024 | Review and ComparisonWindows ASP.NET Core Hosting 2024 | Review and Comparison

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 from the IoC container and using its methods to send messages to the hub’s clients.

Scale-out

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.

Reconnections

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.

Get Started with ASP.NET Core SignalR

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.

Create the Initial Application

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:

dotnet new razor --auth Individual

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.

dotnet add package Microsoft.AspNetCore.SignalR --version 1.0.0-rc1-final

Add a SignalR Hub

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:

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);
        }
    }
}

The SendMessage method is invoked by the clients whenever a message needs to be sent. It uses the 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.

For the hub to function, SignalR needs to be enabled in the application. To do this, make a few changes to Startup.cs.

In 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");
    });
}

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 @@):

<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>

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.

<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>

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.

Add Authentication and Authorization

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.

[Authorize]
public class Chat : Hub
{
    public async Task SendMessage(string message)
    {
        await Clients.All.SendAsync("newMessage", Context.User.Identity.Name, message);
    }
}

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.

Add JSON Web Token Security

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 access_token.

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).

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);
    });
});

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.

//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; }
    }
}

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.

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class Chat : 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.

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();
    });
}

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.

[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

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.

dotnet add package Microsoft.AspNetCore.SignalR.Client --version 1.0.0-rc1-final

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.

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);
            } 
        }
    }
}

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.

Reconnections

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.

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

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.

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; }
    }
}

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.

[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);
    }
}

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.

Conclusion

As you can see, adding real-time Web functionalities to your cross-platform Web applications is easy using ASP.NET Core SignalR.