Building a One-to-One Chat System in ASP.NET Core MVC with SignalR


Welcome, developers, to this article on building a SignalR chat application with a database in ASP.NET Core MVC. In this post, we will walk through creating a one-on-one chat system using ASP.NET Core Web API and SignalR. Throughout this guide, we will use Visual Studio 2022 and focus on implementing private messaging in a realistic and efficient way.

Introduction to the Project

During a recent project, I needed to implement a private one-on-one chat system. While searching online, I found that most examples provided only code for public chat systems, leaving out examples for private chats. This gap inspired me to write this post to help others implement a one-to-one chat system using ASP.NET Core.

By the end of this post, you will understand how to set up a private chat where messages are delivered only to the intended recipient, ensuring privacy and reliability.


Let's get started!

In my search on the internet, I found that almost all examples provided code for public chat systems and none shared code for private chat systems. 

    That's why I decided to write an article on that topic. In this post, we will explore the creation of a chat application using the latest version of ASP.NET Core with Visual Studio 2022. So, let's begin understanding how we can create a one-to-one chat system in ASP.NET Core MVC with SignalR.

    I have also provided the link to download the source code for that implementation, along with the client-side code, at the end of the post.

    Step 1: Create a New ASP.NET Core Web API Project

    First, open Visual Studio 2022 and create a new ASP.NET Core Web API project. This will be the foundation for our SignalR chat application.



    Step 2: Create a Hub Class

    In your project, add a new class called ChatingHub.cs. This class will inherit from the Hub class in SignalR, allowing us to manage real-time connections between clients.

    Public Chat Code

    Here's the code for a public chat system, where all users receive messages sent by anyone connected to the SignalR hub:
    public class ChatHub : Hub
    {
        public async Task SendMessage(string user, string message)
        {
            await Clients.All.SendAsync("ReceiveMessage", user, message);
        }
    }
    

    However, in a one-to-one chat system, we need to ensure that messages are sent privately between users. A simple broadcast using Clients.All sends messages to all connected users, which is not ideal for private conversations.

    Step 3: Implementing One-to-One Chat

    To implement private messaging, we can create a unique group for each user. The group name will be based on a uniqueChatId for each user. This approach ensures that messages are only delivered to the intended recipient, even if the user is logged in on multiple devices or browsers.

    Here’s the code to modify your ChatingHub.cs for private chat:

    So Copy pasted the below code in your Hub class
    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.SignalR;
    using System.Diagnostics;
    
    namespace SignalRDemo.Chat
    {
        [AllowAnonymous]
        public class ChatingHub : Hub
        {
            public override Task OnDisconnectedAsync(Exception exception)
            {
                Debug.WriteLine("Client disconnected: " + Context.ConnectionId);
                return base.OnDisconnectedAsync(exception);
            }
            public override Task OnConnectedAsync()
            {
                Debug.WriteLine("Client connected: " + Context.ConnectionId);
                return base.OnConnectedAsync();
            }
            //Create Group for each user to chat sepeartely
            public void SetUserChatGroup(string userChatId)
            {
                var id = Context.ConnectionId;
                Debug.WriteLine($"Client {id} added to group " + userChatId);
                Groups.AddToGroupAsync(Context.ConnectionId, userChatId);
            }
            //Send message to user Group
            public async Task SendMessageToGroup(string senderChatId,string senderName, string receiverChatId, string message)
            {
                await Clients.Group(receiverChatId).SendAsync("ReceiveMessage", senderChatId, senderName, receiverChatId, message);
            }
        }
    }
    


    In this method, we send messages to specific users by their connection IDs. This ensures the message is visible only to the receiver, providing the one-on-one chat functionality we are aiming for.

    Key Considerations
    • Multiple Connections: If a user logs in from multiple devices, each connection is associated with the user’s group. This ensures that messages are delivered to all devices where the user is logged in.
    • Connection Management: The OnConnectedAsync and OnDisconnectedAsync methods help manage users joining and leaving the chat groups based on their connection status.

    4.Setting Up SignalR in ASP.NET Core

    You can install the missing package using NuGet Package Manager or the .NET CLI. Make sure to install the appropriate version of the Microsoft.AspNetCore.SignalR package that matches your .NET Core project version to resolve the missing namespace error.

    In the NuGet Package Manager Console, run the following command to install the SignalR package:

    Install-Package Microsoft.AspNetCore.SignalR
    4.1. Configure SignalR: In the program.cs file, add SignalR services and endpoints.

    using SignalRDemo.Chat;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    builder.Services.AddControllers();
    builder.Services.AddSignalR();
    builder.Services.AddSingleton<ChatingHub>();
    
    // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {
        app.UseSwagger();
        app.UseSwaggerUI();
    }
    
    app.UseCors(builder => builder
        .AllowAnyHeader()
        .AllowAnyMethod()
        .SetIsOriginAllowed((host) => true)
        .AllowCredentials()
    );
    app.UseHttpsRedirection();
    
    app.UseAuthorization();
    
    app.MapControllers();
    app.MapHub<ChatingHub>("/ChatingHub");
    app.Run();

    If you have a lower .NET Core version, i.e., less than 7(6,5,3.2 etc), you can configure SignalR in the `Startup.cs` file using the following code:
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
    
        public IConfiguration Configuration { get; }
    
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Register the Swagger services      
            services.AddOpenApiDocument(c =>
            {
                c.Title = "AdequateTravel Travel API";
            });
            services.AddCors(o => o.AddPolicy("AllowOrigin", builder =>
            {
                builder.AllowAnyOrigin()
                       .AllowAnyMethod()
                       .AllowAnyHeader();
            }));
    
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
            services.AddSignalR();
            services.AddDbContext<AdequateDbContext>(item => item.UseSqlServer(Configuration.GetConnectionString("myconn")));
            services.AddSingleton<ChatingHub>();
        }
    
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStaticFiles(); // For the wwwroot folder
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseHsts();
            }
    
            app.UseCors(builder => builder
                .AllowAnyHeader()
                .AllowAnyMethod()
                .SetIsOriginAllowed((host) => true)
                .AllowCredentials()
            );
            app.UseHttpsRedirection();
            // Make sure you call this before calling app.UseMvc()
            app.UseSignalR(routes =>
            {
                routes.MapHub<ChatingHub>("/ChatingHub");
            });
        }
    }
    4.2. Enable CORS For JavaScript 
    Let me explain that functions:
    app.UseCors(builder => builder
                    .AllowAnyHeader()
                    .AllowAnyMethod()
                    .SetIsOriginAllowed((host) => true)
                    .AllowCredentials()
                  );

    5. Client-side Implementation: 

    In this section, we will implement the client-side code using JavaScript with the SignalR client library. This setup is perfect for building real-time applications, such as chat systems.

    To test our one-to-one chat system, we’ll create a simple HTML page. We will then open this page in three different browsers: Chrome, Firefox, and Edge. Each browser acts as a unique user, allowing us to simulate a real chat environment.

    Once we have the HTML page loaded in all three browsers, we can start chatting with each other. This approach is a straightforward way to verify that our SignalR implementation works effectively for real-time communication. By interacting through different browsers, we can ensure that messages are sent and received seamlessly.

    This testing scenario is not just practical but also an engaging way to see how our chat system performs in a real-world setup. Whether you're a developer or a tech enthusiast, experimenting with multiple browser instances provides valuable insights into real-time chat functionality.

    Now we are going to implement the client-side code using e.g., JavaScript with SignalR client library.




    Chrome Browser:

    In this setup, I’ve configured the senderChatId to "Chrome_User" and made some user interface adjustments. For the Chrome user, both Firefox and Edge are shown as available options for chat.

    As illustrated in the image, I created a chat list that displays all users available for conversation. When you click on a username, a chat window opens up, allowing you to initiate a conversation effortlessly. This simple yet effective design enhances the user experience by making it easy to connect with others in real time.



    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Chat</title>
        <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
        <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200" />
        <link rel="stylesheet"
              href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" />
        <style type="text/css">
            body {
                overflow: hidden;
            }
    
            .chat-list {
                max-height: 100vh;
                overflow-y: auto;
            }
    
            .user {
                padding: 10px;
                border-bottom: 1px solid #ddd;
                cursor: pointer;
                display: flex;
                align-items: center;
            }
    
                .user img {
                    width: 40px;
                    height: 40px;
                    object-fit: cover;
                    border-radius: 50px;
                    margin-right: 5px;
                }
    
                .user:hover {
                    background-color: #f0f0f0;
                }
    
            .chat-window {
                position: absolute;
                bottom: 0;
                right: 0;
                width: 350px;
                height: 425px;
                background-color: #fff;
                border: 1px solid #ddd;
                border-top-left-radius: 10px;
                border-top-right-radius: 10px;
                display: none;
            }
    
            .chat-header {
                background-color: #f0f0f0;
                padding: 10px;
                border-bottom: 1px solid #ddd;
            }
    
            .chat-body {
                height: 300px;
                overflow-y: auto;
                padding: 10px;
            }
    
            .chat-footer {
                display: flex;
                padding: 15px 10px;
                border-top: 1px solid #ddd;
            }
    
            .close {
                float: right;
                cursor: pointer;
            }
    
            .chat-ui {
                list-style: none;
                padding: 0px;
                margin: 0;
            }
    
                .chat-ui li {
                    display: flex;
                    align-items: center;
                    margin-bottom: 15px;
                }
    
                    .chat-ui li img {
                        width: 40px;
                        height: 40px;
                        border-radius: 50px;
                        object-fit: cover;
                        object-position: center;
                    }
    
                    .chat-ui li .text {
                        display: flex;
                        flex-direction: column;
                    }
    
                        .chat-ui li .text span {
                            font-size: 12px;
                        }
    
            li.right {
                justify-content: end;
                text-align: right;
            }
    
            .chatbox {
                margin-bottom: 5%;
                padding: 20px;
                border: 1px solid #e1e1e1;
                box-shadow: 0 15px 35px -15px #e1e1e1;
                border-top: 10px solid #68798f;
            }
            .chatlisthead {
                background: #7ea67e;
                padding: 2px;
            }
        </style>
    </head>
    
    <body>
    
        <div class="container-fluid">
            <div class="row">
                <div class="col-8 position-relative min-vh-100">
                    <h4 style="text-align: center;">Login User:<i class="fa fa-chrome" aria-hidden="true"></i> Chrome User</h4>
                    <div class="chat-window" id="chat-window">
                        <div class="chat-header">
                            <span class="close" id="close-chat">×</span>
                            <h4>Chat with <span id="chat-user">User</span></h4>
                        </div>
                        <div class="chat-body">
                            <ul class="chat-ui" data-chatuserid="" id="chatlist">
                                <li class="left">
                                    <span class="material-symbols-outlined">
                                        person
                                    </span>
                                    <div class="text">
                                        Hey, 's it going?
                                        <span>2 min</span>
                                    </div>
    
                                </li>
                                <li class="right">
    
                                    <div class="text">
                                        Not too bad, just chilling. You?
                                        <span>2 min</span>
                                    </div>
                                    <span class="material-symbols-outlined">
                                        person
                                    </span>
                                </li>
                            </ul>
                        </div>
                        <div class="chat-footer">
                            <input type="text" class="form-control" id="textmessage" placeholder="Type a message...">
                            <button class="btn btn-primary ml-1" onclick="SendMessage()">Send</button>
                        </div>
                    </div>
                </div>
                <div class="col-4 chatbox">
                    <h5 class="chatlisthead">Chat List</h5>
                    <div class="chat-list">
                        <div class="user" data-user-id="FireFox_User">
                            <i class="fa fa-firefox" aria-hidden="true"></i> FireFox_User
    
                        </div>
                        <div class="user" data-user-id="Edge_User">
                            <i class="fa fa-edge" aria-hidden="true"></i> Edge_User
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.js"></script>
        <link href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.0.1/css/toastr.css" rel="stylesheet" />
        <script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.0.1/js/toastr.js"></script>
        <script>
            $(document).ready(function () {
                $(".user").click(function () {
                    var userId = $(this).data("user-id");
                    var userName = $(this).text();
                    $("#chat-user").text(userName);
                    $("#chat-window").slideDown();
                    $("#chatlist").attr("data-chatuserid", userId);
                    $("#chatlist").empty();
                });
    
                $("#close-chat").click(function () {
                    $("#chat-window").slideUp();
                });
            });
            var senderChatId = "Chrome_User"; //Chrome user unique chatId
            const connection = new signalR.HubConnectionBuilder()
                .withUrl("https://localhost:7039/ChatingHub")
                .configureLogging(signalR.LogLevel.Information)
                .build();
    
            async function start() {
                try {
                    await connection.start();
                    console.log("SignalR Connected.");
                    //Creating user group with his unique chatId
                    await connection.invoke("SetUserChatGroup", senderChatId);
                } catch (err) {
                    console.log(err);
                    setTimeout(start, 5000);
                }
            };
    
            connection.on("ReceiveMessage", async (senderId, senderName, reciverId, message) => {
                var messageBuilder = "<li class=''><div class=''><div class=''>" + GetUserName(senderId, senderName) +"</div><small>"+message+ "</small>" + "</li>"
                $("#chatlist").append(messageBuilder);
    
                //Showing notifcation to user if get any message
                var notification = "You have received a message from user " + senderName;
                toastr.success(notification);
            });
    
            connection.onclose(async () => {
                await start();
            });
    
            // Start the connection.
            start();
    
            async function SendMessage() {
                try {
                    var message = $("#textmessage").val();
                    if (message) {
    
                        //Getting reciver unique chatId for sending message to reciver user chat Group so that others user can't recived it
                        var reciverId = $("#chatlist").attr("data-chatuserid");
                        var senderName = senderChatId;
                        await connection.invoke("SendMessageToGroup", senderChatId, senderName, reciverId, message);
                        var messageBuilder = "<li class='right'><div class='text'><div class='user'>" + GetUserName(reciverId, senderName) + "</div><small>" + message + "</small>" + "</li>"
                        $("#chatlist").append(messageBuilder);
                        $("#textmessage").val("");
                    }
                    else { 
    
                        toastr.error("Please input message!");
                    }
                } catch (err) {
                    console.error(err);
                }
            }
            //Function for getting username and icon when binding message to the chat list
            function GetUserName(userChatId, userName) {
                if (userChatId == "Edge_User") {
                    return '<i class="fa fa-edge" aria-hidden="true"></i>' + userName;
                }
                else if (userChatId == "Chrome_User") {
                    return '<i class="fa fa-chrome" aria-hidden="true"></i>' + userName;
                }
                else if (userChatId == "FireFox_User") {
                    return '<i class="fa fa-firefox" aria-hidden="true"></i>' + userName;
                }
            }
        </script>
    </body>
    
    </html>
    

    Edge Browser User 

    Here, I have set the senderChatId to "Edge_User" and made some UI changes. For example, for the Edge user, Firefox and Chrome are available for chat.

    Firefox Browser User 

    Here, I have set the `senderChatId` to "FireFox_User" and made some UI changes. For example, for the Firefox user, Edge and Chrome are available for chat.

    Downlaod Source Code:

    If you’re looking for the source code, you can find it on GitHub. Here’s the link: GitHub - SignalR One-To-One Chat.

    Now, let’s open all three pages in different browsers based on the client-side code and start chatting.

    For example, if the users in Edge and Chrome want to chat, the user in the Chrome browser will click on the Edge username in the chat list. This action opens a chat box. Similarly, the user in the Edge browser will click on the Chrome username in their chat list.

    Now, you can send messages between the two clients. You’ll notice that the messages sent from one browser will appear in the other, allowing you to verify that everything works smoothly. Additionally, any message sent will also show up for the user in Firefox, demonstrating real-time communication across all browsers.
    For instance, if the users in Firefox and Chrome want to chat, the user in the Chrome browser will click on the Firefox username in the chat list, which will open a chat box. Likewise, the user in the Firefox browser will click on the Chrome username in their chat list.

    Now you can send messages from one client browser to the other. As you do this, you can verify that the messages sent will also appear in the Edge browser, showcasing the seamless real-time communication between all users involved.