George Kosmidis

Microsoft MVP | Speaks of Azure, AI & .NET | Founder of Munich .NET
Building tomorrow @
slalom
slalom

Building Real-Time Apps with SignalR in .NET

by George Kosmidis / Published 1 year ago
Building Real-Time Apps with SignalR in .NET

Introduction

In the digital age, the demand for interactive, real-time web applications is more prominent than ever. From live chat systems to instant notifications, users expect seamless, immediate interactions when they navigate online platforms. This is where SignalR, a powerful .NET library, steps in to revolutionize how developers build these dynamic experiences.

What is SignalR?

SignalR is an open-source library that simplifies the process of adding real-time web functionality to applications. It enables two-way communication between the server and the client in real-time. As a part of the .NET ecosystem, SignalR seamlessly integrates with existing .NET applications, offering a robust solution for developing real-time features.

Key Features and Benefits

  • Real-Time Communication: SignalR facilitates instant data exchange, enabling features like chat systems, live feeds, and notifications.
  • Scalability: Easily scales to accommodate a high number of connections, making it suitable for large-scale applications.
  • Fallback Mechanisms: It intelligently falls back to older technologies if WebSockets are not supported, ensuring wide compatibility.
  • High-Level Abstractions: SignalR abstracts complex networking and concurrency issues, allowing developers to focus on core functionality.

Getting Started with SignalR

Prerequisites

  • Basic knowledge of .NET.
  • Visual Studio or another .NET-compatible IDE.
  • Latest .NET Core SDK installed on your machine.

Setting Up a New .NET Project with SignalR

  1. Create a new .NET Core Web Application.
  2. Install the Microsoft.AspNetCore.SignalR NuGet package.
  3. Configure SignalR in the Startup.cs file.

Core Concepts of SignalR

Hubs

Hubs in SignalR serve as the main conduit for communication between clients and the server. They are designed to simplify the process of sending messages back and forth, abstracting the complexities of connection management and method invocation. Here's a closer look at what makes Hubs a central piece of the SignalR architecture:

  • Method Invocation: Hubs allow the server to call methods on clients and vice versa. This means you can easily call JavaScript functions from C# code on the server and C# methods from JavaScript in the client.
  • Strong Typing: SignalR provides a strongly typed hub interface, enabling developers to define methods on the server that clients can call, leading to compile-time checking and IntelliSense support in IDEs.
  • Group Management: Hubs facilitate adding or removing connections to named groups, allowing for targeted messaging to subsets of connected clients, such as a chat room or a specific user category.

Connections

The concept of connections is fundamental to understanding SignalR. Each client connecting to a SignalR hub establishes a "connection" represented by a unique connection ID. SignalR abstracts the connection details, allowing developers to focus on the application logic rather than the intricacies of real-time communication protocols.

  • Connection Lifecycle: SignalR manages the lifecycle of connections, handling events like connect, disconnect, and reconnect. Developers can hook into these events to perform actions, such as updating a user list when someone joins or leaves a chat.
  • Transport Fallbacks: SignalR uses WebSockets as its primary transport but can fall back to other techniques (like server-sent events or long polling) if necessary. This ensures compatibility across a wide range of browsers and network environments.
  • Connection Security: SignalR integrates with ASP.NET Core's security models, allowing the use of authentication and authorization mechanisms to control access to hubs and methods based on the connected user.

Messages

Messages are the payload of information exchanged between clients and servers. SignalR simplifies the process of sending messages, whether it's broadcasting to all connected clients, targeting specific groups, or sending direct messages to individual connections.

  • Broadcasting: Sending a message to all connected clients simultaneously. This is useful for features like announcements or live updates.
  • User and Group Messages: SignalR allows messages to be sent to specific groups or users. This is particularly useful for applications like chat rooms, where you might want to send a message to everyone in a particular room or direct messages between users.
  • Client-to-Server and Server-to-Client: Messages can flow in both directions. Clients can invoke methods on the server, and the server can send messages to one or more clients.
  • Performance Considerations: While sending messages is straightforward, developers must consider the impact of message size and frequency on performance and scalability, especially in applications with a large number of clients.

Building a Real-Time Chat Application with SignalR

This tutorial will guide you through creating a feature-rich chat application using SignalR, covering both the basics and some advanced functionalities like handling user connections/disconnections and enabling private messaging.

Server-Side (Hub) Implementation

The following C# code defines a ChatHub class that inherits from SignalR's Hub class, which is a central part of building real-time, interactive applications using SignalR.

public class ChatHub : Hub
{
    // Notifies all connected clients when a new user connects to the chat
    public override async Task OnConnectedAsync()
    {
        await Clients.All.SendAsync("ReceiveMessage", "System", $"{Context.ConnectionId} joined the chat");
        await base.OnConnectedAsync();
    }

    // Notifies all connected clients when a user disconnects from the chat
    public override async Task OnDisconnectedAsync(Exception exception)
    {
        await Clients.All.SendAsync("ReceiveMessage", "System", $"{Context.ConnectionId} left the chat");
        await base.OnDisconnectedAsync(exception);
    }

    // Sends a message to all connected clients from a specific user
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }

    // Sends a private message to a specific client, identified by connection ID
    public async Task SendPrivateMessage(string toConnectionId, string message)
    {
        await Clients.Client(toConnectionId).SendAsync("ReceiveMessage", "Private", message);
    }
}

Let's break down what each part of this code does:

  1. OnConnectedAsync Method
  • Purpose: Automatically called when a new client successfully establishes a connection with the SignalR server.
  • Functionality: When a client connects, this method sends a message to all connected clients (including the one just connected) notifying them that someone has joined the chat. It uses Clients.All.SendAsync to broadcast the message to every connected client.
  • Context.ConnectionId: A unique identifier for the connected client, used here to indicate which client has joined the chat.
  1. OnDisconnectedAsync Method
  • Purpose: Invoked when a client disconnects from the SignalR server, whether due to closing their browser, losing connection, or if the server disconnects them.
  • Functionality: Similar to OnConnectedAsync, it broadcasts a message to all connected clients that a particular client (identified by Context.ConnectionId) has left the chat.
  • Exception Handling: The method takes an Exception parameter that can be used to determine if the disconnection was due to an error.
  1. SendMessage Method
  • Purpose: Allows a client to send a message to all connected clients.
  • Functionality: This method is called by a client wishing to broadcast a message. It takes two parameters: user (the name or identifier of the sender) and message (the content to be broadcast). It then uses Clients.All.SendAsync to distribute the message to all clients, including the sender.
  1. SendPrivateMessage Method
  • Purpose: Enables sending a private message to a specific client identified by their connection ID.
  • Functionality: This method allows sending a message to a single client, identified by the toConnectionId parameter. The message is marked as "Private" to distinguish it from public messages. It uses Clients.Client(toConnectionId).SendAsync to send the message only to the specified client.

Client-Side Implementation

We'll implement a basic UI for displaying messages and a form for sending them, along with connection and disconnection notifications.

HTML Markup

<!DOCTYPE html>
<html>
<head>
    <title>SignalR Chat</title>
</head>
<body>
    <div id="chatWindow"></div>
    <input type="text" id="messageInput" placeholder="Enter your message..." />
    <button id="sendButton">Send</button>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/3.1.7/signalr.min.js"></script>
    <script src="chat.js"></script>
</body>
</html>

JavaScript (chat.js)

const connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();

// Disable the send button until connection is established
document.getElementById("sendButton").disabled = true;

// Display messages in the chat window
connection.on("ReceiveMessage", function(user, message) {
    const msg = document.createElement("div");
    msg.textContent = `${user}: ${message}`;
    document.getElementById("chatWindow").appendChild(msg);
});

// Start the connection
connection.start().then(function() {
    document.getElementById("sendButton").disabled = false;
}).catch(function(err) {
    return console.error(err.toString());
});

// Send message to the server
document.getElementById("sendButton").addEventListener("click", function(e) {
    const message = document.getElementById("messageInput").value;
    connection.invoke("SendMessage", "User", message).catch(err => console.error(err.toString()));
    e.preventDefault(); // Prevent form from submitting
});

// Optionally, handle the enter key
document.getElementById("messageInput").addEventListener("keypress", function(e) {
    if (e.key === "Enter") {
        document.getElementById("sendButton").click();
    }
});

Skipping the HTML Snippet, here's a step-by-step explanation of what's happening in the code:

  1. Initialize SignalR Connection:

    • const connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();
    • This line creates a new SignalR connection to the server. The .withUrl("/chatHub") part specifies the URL endpoint of the SignalR hub on the server. The .build() method finalizes the connection configuration.
  2. Disable Send Button Initially:

    • document.getElementById("sendButton").disabled = true;
    • Before the connection is established, the send button is disabled to prevent users from trying to send messages.
  3. Setting Up a Message Receiver:

    • The connection.on("ReceiveMessage", function(user, message) { ... }); block sets up an event listener that waits for messages from the server. Whenever the server sends a message using the "ReceiveMessage" event, this function is triggered. The function creates a new div element, sets its text content to include the user's name and message, and appends this div to the "chatWindow" element. This way, messages received from the server are displayed to the user.
  4. Establishing the Connection:

    • connection.start().then(function() { ... }).catch(function(err) { ... });
    • This attempts to start the connection to the server. If successful, it enables the send button by setting its disabled property to false, allowing users to send messages. If there's an error (for example, if the server is unreachable), it logs the error to the console.
  5. Sending Messages to the Server:

    • The event listener for the send button (document.getElementById("sendButton").addEventListener("click", function(e) { ... });) listens for click events. When the button is clicked, it retrieves the message from the "messageInput" input field and sends it to the server by calling connection.invoke("SendMessage", "User", message). The "SendMessage" part corresponds to a method on the server-side hub that's designed to receive messages from clients. If there's an error in sending the message, it catches and logs the error. The e.preventDefault(); call prevents the default form submission behavior, keeping the page from reloading.
  6. Handling Enter Key Press:

    • The last part (document.getElementById("messageInput").addEventListener("keypress", function(e) { ... });) adds an event listener to the message input field that listens for keypress events. If the pressed key is "Enter", it programmatically clicks the send button, allowing users to send messages by pressing Enter instead of clicking the button.

Extended Features and Considerations

  • User Authentication and Identification: Integrate ASP.NET Core Identity for meaningful user identification.
  • Message Persistence: Store messages in a database to maintain chat history.
  • Group Management: Add functionality for users to create and join chat rooms or groups, with messages sent only to group members.

Advanced SignalR Features

Working with Groups

Groups in SignalR serve as a means to categorize connections. This feature is invaluable for applications requiring targeted messaging, such as a chat application with private rooms or a notification system where messages are sent to specific subsets of users.

How to Use Groups

  • Creating and Joining Groups: You can dynamically add connections to groups without needing to define them in advance. This is done server-side, typically in your hub class.
public async Task AddToGroup(string groupName)
{
    await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
    await Clients.Group(groupName).SendAsync("Send", $"{Context.ConnectionId} has joined the group {groupName}.");
}
  • Sending Messages to Groups: Once a connection is added to a group, you can send messages to all connections in that group. This allows for efficient communication with specific user segments.
public async Task SendMessageToGroup(string groupName, string message)
{
    await Clients.Group(groupName).SendAsync("ReceiveMessage", message);
}

Handling Connections

Effective connection management is crucial for maintaining the integrity and performance of real-time applications.

Connection Lifecycle Events

  • OnConnectedAsync: Override this method in your hub to perform actions when a new connection is established, such as logging or custom authentication.

  • OnDisconnectedAsync: Use this to clean up when a connection is closed, such as removing users from groups or updating UI elements to reflect the disconnection.

public override async Task OnConnectedAsync()
{
    // Custom logic here
    await base.OnConnectedAsync();
}

public override async Task OnDisconnectedAsync(Exception exception)
{
    // Cleanup logic here
    await base.OnDisconnectedAsync(exception);
}

Implementing connection tracking can be done by maintaining a mapping of user identifiers to connection IDs, enabling targeted communication and connection-specific actions.

Scaling SignalR Applications

For SignalR applications to support thousands to millions of concurrent connections, scaling out is necessary. Azure SignalR Service is designed to handle this by offloading the connection management and scaling requirements from your servers. Although an intro into Azure SignalR worths an article, here is a small spoiler!

How Azure SignalR Service Works

  • Automatic Scaling: Azure SignalR Service automatically scales to accommodate the number of connections, allowing you to focus on application logic rather than infrastructure concerns.

  • Serverless Compatibility: It can operate in a serverless mode, enabling you to build applications without managing servers, further simplifying the development process.

  • Integration with Azure Functions: You can integrate SignalR with Azure Functions to trigger real-time messages in response to events, creating highly responsive and dynamic applications.

Implementing Azure SignalR Service

  1. Provision Azure SignalR Service: Through the Azure portal, create an instance of the Azure SignalR Service and configure it according to your application's needs.

  2. Update Your Application Configuration: Integrate the service into your application by updating the connection strings and configuring your application to use Azure SignalR Service.

  3. Adjust Your Code for Azure SignalR: Ensure your application logic is compatible with the distributed nature of the cloud service, especially regarding connection and group management.

Conclusion

In conclusion, SignalR stands out as a powerful tool in the .NET arsenal for building sophisticated real-time web applications. Its ability to facilitate instantaneous communication between the server and the client opens up endless possibilities for creating interactive, dynamic user experiences. By leveraging SignalR, we can easily implement features such as live chats, real-time updates, and interactive games, significantly enhancing the user engagement and satisfaction.

This page is open source. Noticed a typo? Or something unclear?
Edit Page Create Issue Discuss
Microsoft MVP - George Kosmidis
Azure Architecture Icons - SVGs, PNGs and draw.io libraries