Advance Chat Application with .NET and SignalR

Introduction

In this blog, we will explore the development of an advanced real-time chat application that facilitates communication both between individual users (one-to-one) and within user groups (one-to-many). The application is built using .NET 8, SignalR for real-time communication, and MS-SQL as the database. We will leverage ASP.NET Identity roles to create user groups, enabling targeted messaging within these groups.

Please follow SignalR's official documentation for more information.

Prerequisites

  • Visual Studio 2022 installed,
  • Microsoft SQL Server 18,
  • Basics of Asp .Net Web, SignalR, Javascript

Source Code

This source code is publicly available on the GitHub link.

Step 1. Open Visual Studio 2022 and create a new MVC Project.

MVC Project

Step 2. Right-click on Solution Add new Scafolded Items > Identity > Select all the features > Add.

 Scafolded Items

Add identity

Step 3. Go to the App settings and change your connection string accordingly.

"ConnectionStrings": {
  "AppDbContextConnection": "Data Source = DESKTOP-R22RJF3\\SQLEXPRESS; Database = SignalRMVC; Integrated Security = True; Connect Timeout = 30; Encrypt = False; TrustServerCertificate = False; ApplicationIntent = ReadWrite; MultiSubnetFailover = False"
}  

Step 4. Add _loginPartial view to the navbar of the _Layout.cshtml page.

<partial name="_LoginPartial" />

Navbar

Step 5. Open the Package Manager console, then add a new migration. This will create a migration for the

add-migration FirstMigration

Step 6. Type the update database in the Package Manager console. This will create a database.

Database

Step 7. Add the following line in the program.cs

app.MapRazorPages();

Step 8. Run the Application.

Application

Step 9. Click on Register. Register the four users as [email protected], [email protected], [email protected], [email protected]. And click on Confirm Email.

User

If you look into the database, four users have been created as below.

Message

Add two roles, User and Manager, into the AspNetRoles table.

INSERT INTO [dbo].[AspNetRoles]
           ([Id],[Name],[NormalizedName])
     VALUES
           ('43064954-d35d-49ef-9cf2-abe84345e891','User','USER'),
		   ('c87fa796-d513-4c1b-aef5-2bfd73e7a439','Manager','MANAGER')

Then, map users to the role in the AspNetUserRoles table.

 Insert into AspNetUserRoles values 
		   ('4ba40cf0-c224-43f9-9d16-406662ebcc56','c87fa796-d513-4c1b-aef5-2bfd73e7a439'),
		   ('888c4fbf-3649-4ac0-a71c-12e4bfccf63a','c87fa796-d513-4c1b-aef5-2bfd73e7a439'),
		   ('6876a6c7-e3be-4d44-8857-f619f27ce295','43064954-d35d-49ef-9cf2-abe84345e891'),
		   ('ac0d1466-9a7c-43f1-a360-06d75b30a739','43064954-d35d-49ef-9cf2-abe84345e891')

Note. Please replace UserId according to the ID of the AspNetUsers table.

Step 10. Log in to the App via the credentials of user1.

This will open a home page as below.

Home page

Step 11. Go to the solution explorer and add a new class RoleViewModel.

public class RoleViewModel
{
    public IList<string> UserRoles { get; set; }
}

Step 12. Go to the Solution Explorer > Views > Home > index. cshtml and paste the code below.

@model RoleViewModel

<div class="container">
    <div class="row">&nbsp;</div>

    <div class="row">
        <div class="col-3">Role</div>
        <div class="col-6" id="role" style="color:blue">@Model.UserRoles?.FirstOrDefault()</div>
    </div>
    <div class="row">
        <div class="col-3">Sender</div>
        <div class="col-6"><input class="col-12" type="text" value="@User.Identity?.Name" id="senderEmail" disabled /></div>
    </div>
    <div class="row">
        <div class="col-3">Receiver</div>
        <div class="col-6"><input class="col-12" type="text" id="receiverEmail" /></div>
    </div>
    <div class="row">
        <div class="col-3">Message</div>
        <div class="col-6"><input class="col-12" type="text" id="chatMessage" /></div>
    </div>
    <div class="row">&nbsp;</div>
    <div class="row">
        <div class="col-6">
            <input type="button" id="sendMessage" value="Send Message" />
        </div>
        <div class="col-6">
            <input type="button" id="sendMessageToGroup" value="Send Message to Group" />
        </div>
    </div>
    <div class="row">
        <div class="col-12">
            <hr />
        </div>
    </div>
    <div class="row">
        <div class="col-6">
            <ul id="messagesList"></ul>
        </div>
    </div>

</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/8.0.7/signalr.js"></script>
<script>

    var connectionChat = new signalR.HubConnectionBuilder().withUrl("/hubs/basicchat").build();

    document.getElementById("sendMessage").disabled = false;
    connectionChat.on("MessageReceived", function (user, message) {
        var li = document.createElement("li");
        document.getElementById("messagesList").appendChild(li);
        li.textContent = `${user} - ${message}`;
    });

    document.getElementById("sendMessage").addEventListener("click", function (event) {
        var sender = document.getElementById("senderEmail").value;
        var message = document.getElementById("chatMessage").value;
        var receiver = document.getElementById("receiverEmail").value;
        if (receiver.length > 0) {

            $.ajax({
                url: '/SendMessageToReceiver',
                type: 'GET',
                data: { sender: sender, receiver: receiver, message: message },
                success: function (response) {
                    console.log(response);
                },
                error: function (error) {
                    console.error('Error:', error);
                }
            });
        }
        else {
            //send message to all of the users

            $.ajax({
                url: '/SendMessageToAll',
                type: 'GET',
                data: { user: sender, message: message },
                success: function (response) {
                    console.log(response);
                },
                error: function (error) {
                    console.error('Error:', error);
                }
            });

        }
        event.preventDefault();
    })

    document.getElementById("sendMessageToGroup").addEventListener("click", function (event) {
        var message = document.getElementById("chatMessage").value;

        $.ajax({
            url: '/SendMessageToGroup',
            type: 'GET',
            data: { message: message },
            success: function (response) {
                console.log(response);
            },
            error: function (error) {
                console.error('Error:', error);
            }
        });

        event.preventDefault();
    })

    connectionChat.start().then(function () {
        var sender = document.getElementById("senderEmail").value;
        connectionChat.send("JoinGroup", sender);
        document.getElementById("sendMessage").disabled = false;
    });
</script>

This code includes a chat application form that connects to a SignalR hub in the JavaScript section. It adds the user to a group upon connection and allows the user to send messages either to an individual or to a group. Messages are sent via AJAX requests and displayed in real-time.

Step 13. Add a new class called BasicChatHub.cs and paste the below code.

public class BasicChatHub : Hub
{
    private readonly UserManager<IdentityUser> _userManager;
    public BasicChatHub(UserManager<IdentityUser> userManager)
    {
        _userManager = userManager;
    }
    public override async Task OnConnectedAsync()
    {
        var httpContext = Context.GetHttpContext();
        var userId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        var roles = await GetUserRoles(userId);
        // Now you can use the userId as needed
        await base.OnConnectedAsync();
    }

    public string GetUserId()
    {
        var httpContext = Context.GetHttpContext();
        var userId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        return userId;
    }

    public async Task<IList<string>> GetUserRoles(string userId)
    {
        var user = await _userManager.FindByIdAsync(userId);
        var roles = await _userManager.GetRolesAsync(user);
        return roles;
    }

    public static List<string> GroupsJoined { get; set; } = new List<string>();

    [Authorize]
    public async Task JoinGroup(string sender)
    {
        var user = GetUserId();
        var role = (await GetUserRoles(user)).FirstOrDefault();
        if (!GroupsJoined.Contains(Context.ConnectionId + ":" + role))
        {
            GroupsJoined.Add(Context.ConnectionId + ":" + role);
            //do something else
            await Groups.AddToGroupAsync(Context.ConnectionId, role);
        }
    }

}

Step 14. Paste the below code in the HomeController.

public class HomeController : Controller
{
    private readonly AppDbContext _db;
    private readonly IHubContext<BasicChatHub> _basicChatHub;
    private readonly UserManager<IdentityUser> _userManager;
    public HomeController(
        AppDbContext context,
        UserManager<IdentityUser> userManager,
        IHubContext<BasicChatHub> basicChatHub)
    {
        _db = context;
        _userManager = userManager;
        _basicChatHub = basicChatHub;
    }
    [Authorize]
    public async Task<IActionResult> Index()
    {

        var model = new RoleViewModel();
        var user = await _userManager.GetUserAsync(User);
        if(user is not null)
        {
            var roles = await _userManager.GetRolesAsync(user);
            model.UserRoles = roles;
        }
        return View(model);

    }
    [HttpGet("SendMessageToAll")]
    [Authorize]
    public async Task<IActionResult> SendMessageToAll(string user, string message)
    {
        await _basicChatHub.Clients.All.SendAsync("MessageReceived", user, message);
        return Ok();
    }
    [HttpGet("SendMessageToReceiver")]
    [Authorize]
    public async Task<IActionResult> SendMessageToReceiver(string sender, string receiver, string message)
    {
        var userId = _db.Users.FirstOrDefault(u => u.Email.ToLower() == receiver.ToLower())?.Id;

        if (!string.IsNullOrEmpty(userId))
        {
            await _basicChatHub.Clients.User(userId).SendAsync("MessageReceived", sender, message);
        }
        return Ok();
    }
    [HttpGet("SendMessageToGroup")]
    [Authorize]
    public async Task SendMessageToGroup(string message)
    {
        var user = GetUserId();
        var role = (await GetUserRoles(user)).FirstOrDefault();
        var username = _db.Users.FirstOrDefault(u => u.Id == user)?.Email ?? "";
        if (!string.IsNullOrEmpty(role))
        {
            await _basicChatHub.Clients.Group(role).SendAsync("MessageReceived", username, message);
        }
    }
    private string GetUserId()
    {
        var userId = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        return userId;
    }
    private async Task<IList<string>> GetUserRoles(string userId)
    {
        var user = await _userManager.FindByIdAsync(userId);
        var roles = await _userManager.GetRolesAsync(user);
        return roles;
    }

}

SendMessageToReceiver sends a message directly to a specific user by looking up their email and using SignalR to deliver the message. SendMessageToGroup sends a message to all users in a specific role group, broadcasting the message to everyone in that group. Both methods are secured with authorization to ensure that only authenticated users can send messages.

Step 15. In the program.cs file, paste the following code.

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using SignalRMVC;
using SignalRMVC.Areas.Identity.Data;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("AppDbContextConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<AppDbContext>();
builder.Services.AddControllersWithViews();
//var connectionAzureSignalR = "Endpoint=https://dotnetmastery.service.signalr.net;AccessKey=m9Enl0s8dqQGU6l0M0SGDqjwMttKvqN84hX+acKepmU=;Version=1.0;";
//builder.Services.AddSignalR().AddAzureSignalR(connectionAzureSignalR);
builzer.Services.AddSignalR();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
   //app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.MapHub<BasicChatHub>("/hubs/basicchat");
app.Run();

Demo

Run the Application. In one browser, login through User1 Credentials, and in Another browser, login via Manager1 Credentials. Send the message to Manager1 and click on the Send Message Button, as mentioned in the screenshot below.

Browser

Now, Manager1 will see the message sent by User1.

Manager

If you need to send a message to all the users having a user role, log in with user2 credentials in another browser. Again, type the message for all the users and click on Send Message to the Group.

MVC

Now, all the users with user roles can see the management sent by User 1 to the user group.

User role