Welcome to a SignalR guide for implementing a chat application with features such as displaying user online/offline status, marking message status as seen, and maintaining chat conversation history in the database.
public class ChatHub : Hub
{
// Dictionary to store online status of users
private readonly Dictionary<string, bool> _onlineUsers = new Dictionary<string, bool>();
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
public async Task MarkMessageAsSeen(string messageId)
{
//Write logic for MarkMessageAsSeen
}
public override async Task OnConnectedAsync()
{
_onlineUsers[Context.ConnectionId] = true;
await Clients.All.SendAsync("UserStatusChanged", GetUserStatuses());
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
_onlineUsers.Remove(Context.ConnectionId);
await Clients.All.SendAsync("UserStatusChanged", GetUserStatuses());
await base.OnDisconnectedAsync(exception);
}
private Dictionary<string, bool> GetUserStatuses()
{
return _onlineUsers.ToDictionary(entry => entry.Key, entry => entry.Value);
}
}
In in above we have ChatHub class, we've set up a hub to manage real-time communication between users. We've cerated a private dictionary named `_onlineUsers` to keep track of the online status of users, mapping their unique identifiers to a boolean indicating their online status.
When a user sends a message using the `SendMessage` method, we send it to all clients by invoking the `ReceiveMessage` method for all clients.
For handling the marking of messages as seen, we've defined the `MarkMessageAsSeen` method. Whenever a user connects to the hub, we update their online status to true and inform all clients about the change by invoking the `UserStatusChanged` method and passing the current user statuses obtained from `GetUserStatuses`. We make sure to call the base `OnConnectedAsync` method to ensure proper hub connection handling.
Similarly, when a user disconnects, we remove their entry from the `_onlineUsers` dictionary, update user statuses, and inform all clients about the change. Again, we call the base `OnDisconnectedAsync` method to handle disconnection properly.
The `GetUserStatuses` method simply retrieves the current user statuses from the `_onlineUsers` dictionary and returns them as a dictionary mapping user identifiers to their online status.
ChatHub class manages user connections, disconnections, and message broadcasting, for a robust real-time chat application.
We have also provided a download link for the source code at the end of post. You can download the source code.
Let's get started! , Recently, we've been developing a social network application aimed at connecting solo travelers worldwide, facilitating chat and trip planning. We're considering incorporating a couple of new features into the application:
If you're also working on a chat application and have similar questions or concerns, you've come to the right place. Let's discuss each point in detail.
In this post, we will cover the step-by-step process of setting up an ASP.NET Core MVC project, designing the database, creating a DbContext, setting up a SignalR Hub for chat functionality, configuring user authentication and authorization, and creating the user interface for the chat conversation, including showing a list of online users and enabling users to view chat conversation history.
namespace AspCoreMvcSingnalR.DatabaseEntity
{
public class ChatDbContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<UserChatHistory> UserChatHistory { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Data Source=DESKTOP-MFLCOI2;Initial Catalog=OnlineChatDb;User ID=sa;Password=adk@1234;Encrypt=false;");
}
}
public class User
{
public Guid UserId { get; set; }
public string FullName { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public bool IsOnline { get; set; }
public DateTime? DisconnectedAt { get; set; }
}
[Table("UserChatHistory", Schema = "dbo")]
public class UserChatHistory
{
public Guid Id { get; set; }
public virtual User SenderUser { get; set; }
public Guid SenderUserId { get; set; }
public virtual User ReceiverUser { get; set; }
public Guid ReceiverUserId { get; set; }
public string Message { get; set; }
public bool IsSeen { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? SeetAt { get; set; }
}
}
[Authorize]
public class RealTimeChatHub : Hub
{
public override async Task OnDisconnectedAsync(Exception exception)
{
using (var _chatDbContext = new ChatDbContext())
{
//Get logged UserId From the claim
Guid userId = new Guid(Context.User.FindFirstValue(ClaimTypes.PrimarySid));
//Set User status to offline
var user = _chatDbContext.Users.FirstOrDefault(a => a.UserId == userId);
user.IsOnline = true;
user.DisconnectedAt = DateTime.UtcNow;
_chatDbContext.SaveChanges();
}
await Clients.All.SendAsync("UserStatusChanged", "StateChangedLoadLatestChatList");
Debug.WriteLine("Client disconnected: " + Context.ConnectionId);
await base.OnDisconnectedAsync(exception);
}
public override async Task OnConnectedAsync()
{
using (var _chatDbContext = new ChatDbContext())
{
//Get logged UserId From the claim
Guid userId = new Guid(Context.User.FindFirstValue(ClaimTypes.PrimarySid));
//Set User status to online
var user = _chatDbContext.Users.FirstOrDefault(a => a.UserId == userId);
user.IsOnline = true;
_chatDbContext.SaveChanges();
}
await Clients.All.SendAsync("UserStatusChanged", "StateChangedLoadLatestChatList");
await base.OnConnectedAsync();
}
//Create a group for each user to chat separately for private conversations.
public void CreateUserChatGroup(string userId)
{
var id = Context.ConnectionId;
Groups.AddToGroupAsync(Context.ConnectionId, userId);
}
//Mark message as seen
public async Task MarkMessageAsSeen(Guid senderUserId)
{
using (var _chatDbContext = new ChatDbContext())
{
//Set MessageAsSeen
Guid userId = new Guid(Context.User.FindFirstValue(ClaimTypes.PrimarySid));
var userChatHistories = _chatDbContext.UserChatHistory.Where(a => a.ReceiverUserId == userId && a.SenderUserId == senderUserId
&& a.SeetAt == null).ToList();
if (userChatHistories?.Any() ?? false)
{
foreach (var userChat in userChatHistories)
{
userChat.SeetAt = DateTime.UtcNow;
userChat.IsSeen = true;
}
_chatDbContext.SaveChanges();
}
}
}
//Send message to SendMessageToUserChatGroup
public async Task SendMessageToUserChatGroup(string senderUserId, string senderName, string receiverUserId, string message)
{
//Insert message to database then send it to the Client
var _chatDbContext = new ChatDbContext();
UserChatHistory chatHistory = new UserChatHistory();
chatHistory.Message = message;
chatHistory.IsSeen = false;
chatHistory.CreatedAt = DateTime.UtcNow;
chatHistory.SenderUserId = new Guid(senderUserId);
chatHistory.ReceiverUserId = new Guid(receiverUserId);
await _chatDbContext.UserChatHistory.AddAsync(chatHistory);
await _chatDbContext.SaveChangesAsync();
await Clients.Group(receiverUserId).SendAsync("ReceiveMessage",
new ReceiveMessageDTO
{
Message = message,
SenderName = senderName,
SenderUserId = senderUserId
});
}
}
public class ReceiveMessageDTO
{
public string Message { get; set; }
public string SenderUserId { get; set; }
public string SenderName { get; set; }
}
In part , we've implemented a SignalR hub called RealTimeChatHub, which handles real-time communication between clients and the server. We manage user connections and disconnections, update their online status in the database, create chat groups for private conversations, mark messages as seen, and send messages between users. we've defined a ReceiveMessageDTO class to structure the data being sent when receiving messages. using AspCoreMvcSingnalR.DatabaseEntity;
using AspCoreMvcSingnalR.SignalRHub;
using Microsoft.AspNetCore.Authentication.Cookies;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(x => x.LoginPath = "/Home/Index"); //In case of unauhroize user clear auth cookis
builder.Services.AddHttpContextAccessor();
builder.Services.AddSignalR();
builder.Services.AddSingleton<RealTimeChatHub>();
builder.Services.AddDbContext<ChatDbContext>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapControllers();
app.MapHub<RealTimeChatHub>("/RealTimeChatHub");
app.Run();
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
ChatDbContext _chatDbContext;
public HomeController(ILogger<HomeController> logger, ChatDbContext chatDbContext)
{
_logger = logger;
_chatDbContext = chatDbContext;
}
public IActionResult Index()
{
return View();
}
//Here write your own code for login
[HttpPost]
public async Task<IActionResult> Login(LoginViewModel model)
{
// Check if username and password are correct
var user = _chatDbContext.Users.FirstOrDefault(a => a.Email == model.Email && a.Password == model.Password);
if (user != null)
{
// Redirect to chat page
var claims = new List<Claim>() {
new Claim(ClaimTypes.PrimarySid, user.UserId.ToString()),
new Claim(ClaimTypes.Name,user.FullName)
};
//Initialize a new instance of the ClaimsIdentity with the claims and authentication scheme
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
//Initialize a new instance of the ClaimsPrincipal with ClaimsIdentity
var principal = new ClaimsPrincipal(identity);
//SignInAsync is a Extension method for Sign in a principal for the specified scheme.
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, new AuthenticationProperties()
{
IsPersistent = false
});
return RedirectToAction("Chat", "UserChat");
}
else
{
// If login fails, return to the login page with an error message
ModelState.AddModelError(string.Empty, "Invalid username or password");
return View("Index",model);
}
}
}
Index.cshtml
public class LoginViewModel
{
[Required]
public string Email { get; set; }
[Required]
public string Password { get; set; }
}
@model LoginViewModel
@{
ViewData["Title"] = "Home Page";
}
<h1 class="display-4">Enter email and password To Chat</h1>
<form class="form-inline" asp-controller="Home" asp-action="Login" method="post">
<div class="form-group">
<label asp-for="Email"></label>
<input class="form-control" asp-for="Email" />
</div>
<div class="form-group">
<label asp-for="Password"></label>
<input class="form-control" asp-for="Password" type="password" />
</div>
<button type="submit">Login</button>
</form>
We have a HomeController responsible for handling user interactions related to logging in. We inject the logger and the ChatDbContext into the controller. The Index action method renders the login page. The Login action method is call when the user submits the login form. and then we verify the provided credentials against the database. If the credentials are correct, we create claims for the user, sign them in, and redirect them to the chat page. If the login fails, we return to the login page with an error message.
[Authorize]
public class UserChatController : Controller
{
ChatDbContext _chatDbContext;
public UserChatController(ChatDbContext chatDbContext)
{
_chatDbContext = chatDbContext;
}
public IActionResult Chat()
{
User loggedInUser = new User();
//Get logged in user detail from Claim
Guid userId = new Guid(User.FindFirstValue(ClaimTypes.PrimarySid));
string name = User.FindFirstValue(ClaimTypes.Name);
loggedInUser = new User { UserId = userId, FullName = name };
return View(loggedInUser);
}
public ActionResult ChatList()
{
List<User> users = new List<User>();
Guid userId = new Guid(User.FindFirstValue(ClaimTypes.PrimarySid));
users = _chatDbContext.Users.Where(a => a.UserId != userId).ToList();
return PartialView("_ChatList", users);
}
public ActionResult GetChatCobversion(Guid userIdToLoadChat)
{
ChatConversionModel chatConversion = new ChatConversionModel();
Guid loginUserId = new Guid(User.FindFirstValue(ClaimTypes.PrimarySid));
chatConversion.ChatUser = _chatDbContext.Users.FirstOrDefault(a => a.UserId == userIdToLoadChat);
chatConversion.UserChatHistories = _chatDbContext.UserChatHistory.Include("SenderUser")
.Include("ReceiverUser").Where(a => (a.ReceiverUserId == loginUserId && a.SenderUserId == userIdToLoadChat)
|| (a.ReceiverUserId == userIdToLoadChat && a.SenderUserId == loginUserId)).OrderByDescending(a => a.CreatedAt).ToList();
ViewData["loginUserId"] = loginUserId;
return PartialView("_ChatConversion", chatConversion);
}
}
public class ChatConversionModel
{
public User? ChatUser { get; set; }
public List<UserChatHistory>? UserChatHistories { get; set; }
}
we have a UserChatController responsible for managing user chat functionality. The Chat action method renders the main chat page, where we retrieve details of the logged-in user from the claim. The ChatList action method returns the list of users available for chat, excluding the logged-in user. The GetChatCobversion action method retrieves the chat conversation with a specific user, including the user's information and chat history between the logged-in user and the selected user.@model AspCoreMvcSingnalR.DatabaseEntity.User
@{
ViewData["Title"] = "Chat";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<style type="text/css">
.chat-list {
max-height: 100vh;
overflow-y: auto;
}
.message{
font-size:10px;
}
.user {
padding: 10px;
border-bottom: 1px solid #ddd;
cursor: pointer;
display: flex;
align-items: center;
font-size:13px;
}
.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: 60px;
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;
}
.overlay {
position: fixed;
background: #4646462b;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 10000;
}
.min-vh-80{
min-height:80vh;
}
.dot {
background: tomato;
}
.dot, .dot:after {
display: inline-block;
width: 2em;
height: 2em;
border-radius: 50%;
animation: a 1.5s calc(((var(--i) + var(--o, 0))/var(--n) - 1)*1.5s) infinite;
}
.dot:after {
--o: 1;
background: currentcolor;
content: '';
}
@@keyframes a
0%, 50% {
transform: scale(0);
}
</style>
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" />
<div class="container-fluid">
<div class="row">
<div class="col-8 position-relative min-vh-80">
<h4 style="text-align: center;">Login User: @Model.FullName</h4>
<div class="chat-window" id="chat-window">
</div>
</div>
<div class="col-4 chatbox">
<h5 class="chatlisthead">Chat List</h5>
<div class="chat-list" id="divChatList">
</div>
</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.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>
<div class="overlay" id="divloader" style="display:none">
<div class="a" style="--n: 5;position: absolute;top: 50%;left: 50%;">
<div class="dot" style="--i: 0;"></div>
<div class="dot" style="--i: 1;"></div>
<div class="dot" style="--i: 2;"></div>
<div class="dot" style="--i: 3;"></div>
<div class="dot" style="--i: 4;"></div>
</div>
</div>
<script type="text/javascript">
function Loader(_value) {
if (_value) {
document.getElementById("divloader").style.display = "block";
}
else {
document.getElementById("divloader").style.display = "none";
}
setTimeout(function () { document.getElementById("divloader").style.display = "none"; }, 30000);
}
</script>
<script type="text/javascript">
$(document).ready(function () {
LoadUserChatList();
$("#close-chat").click(function () {
$("#chat-window").slideUp();
});
});
var senderChatId = "@Model.UserId";
const connection = new signalR.HubConnectionBuilder()
.withUrl("/RealTimeChatHub")
.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("CreateUserChatGroup", senderChatId);
} catch (err) {
console.log(err);
setTimeout(start, 5000);
}
};
connection.on("UserStatusChanged", function (userStatuses) {
// Update UI to reflect online/offline status
LoadUserChatList();
});
connection.on("ReceiveMessage", async (messageObj) => {
var messageBuilder = "<li class='left'><div class='text'><div class='user'>" + GetUserNameWithIcon(messageObj.senderName) + "</div><small>" + messageObj.message + "</small>" + "</li>"
$("#chatlist").append(messageBuilder);
//If ChatBox is open then MarkMessageAsSeen from senderId
var reciverId = $("#chatlist").attr("data-userid");
if (reciverId)
{
await connection.invoke("MarkMessageAsSeen", messageObj.senderUserId);
}
//Showing notifcation to user if get any message
var notification = "You have received a message from user " + messageObj.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-userid");
var senderName = '@Model.FullName';
await connection.invoke("SendMessageToUserChatGroup", senderChatId, senderName, reciverId, message);
var messageBuilder = "<li class='right'><div class='text'><div class='user'>" + GetUserNameWithIcon(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 GetUserNameWithIcon(userName) {
return '<i class="fa fa-user-circle-o" aria-hidden="true"></i>' + userName;
}
function LoadUserChatCobversion(name) {
Loader(true)
$.ajax({
url: '/UserChat/GetChatCobversion?userIdToLoadChat=' + name,
type: 'GET',
success: function (result) {
$('#chat-window').html(result);
Loader(false)
},
error: function (xhr, status, error) {
console.log(xhr.responseText);
Loader(false)
}
});
}
function LoadUserChatList() {
Loader(true)
$.ajax({
url: '/UserChat/ChatList',
type: 'GET',
success: function (result) {
$('#divChatList').html(result);
Loader(false)
},
error: function (xhr, status, error) {
console.log(xhr.responseText);
Loader(false)
}
});
}
</script>
_ChatList.cshtml
@model IEnumerable<AspCoreMvcSingnalR.DatabaseEntity.User>
@if (Model != null && Model.Any())
{
foreach (var user in Model)
{
<div class="user" data-user-id="@user.UserId">
<i class="fa fa-user-circle-o" aria-hidden="true"></i> @user.FullName
@if(user.IsOnline)
{
<i class="fa fa-circle" style="color:green;font-size: 12px;" aria-hidden="true"></i>
}
else
{
<i class="fa fa-circle" style="color:red;font-size: 12px;" aria-hidden="true"></i>
}
</div>
}
}
<script type="text/javascript">
$(document).ready(function () {
$(".user").click(function () {
var userId = $(this).data("user-id");
var userName = $(this).text();
$("#chat-window").slideDown();
LoadUserChatCobversion(userId);
});
});
</script>
_ChatConversion.cshtml
@model AspCoreMvcSingnalR.Controllers.ChatConversionModel
@{
string loginUserId = ViewData["loginUserId"].ToString();
}
@if (Model != null && Model.ChatUser != null)
{
<div class="chat-header">
<span class="close" id="close-chat">×</span>
<h4>
Chat with <span>
@Model.ChatUser.FullName
@if (Model.ChatUser.IsOnline)
{
<i class="fa fa-circle" style="color:green;font-size: 12px;" aria-hidden="true"></i>
}
else
{
<i class="fa fa-circle" style="color:red;font-size: 12px;" aria-hidden="true"></i>
}
</span>
</h4>
</div>
<div class="chat-body">
<ul class="chat-ui" data-userid="@Model.ChatUser.UserId" id="chatlist">
@if (Model.UserChatHistories?.Any() == true)
{
@foreach (var chat in Model.UserChatHistories)
{
if (chat.SenderUserId.ToString() == loginUserId)
{
//sender is loggged in user then show message to right side
<li class="right">
<div class="text">
<div class="user">
<i class="fa fa-user-circle-o" aria-hidden="true"></i> @chat.SenderUser.FullName
</div>
<small class="message">
@chat.Message
<br>
@if (chat.SeetAt != null)
{
<small style="font-size:8px">SeetAt: @chat.SeetAt.ToString()</small>
}
else
{
<small style="font-size:8px">Not seen</small>
}
</small>
</div>
</li>
}
else
{
<li class="left">
<div class="text">
<div class="user">
<i class="fa fa-user-circle-o" aria-hidden="true"></i> @chat.SenderUser.FullName
</div>
<small class="message">
@chat.Message
<br>
@if (chat.SeetAt != null)
{
<small style="font-size:8px">SeetAt: @chat.SeetAt.ToString()</small>
}
else
{
<small style="font-size:8px">Not seen</small>
}
</small>
</div>
</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>
}
You can download the source code from GitHub: https://github.com/ramkumar457/SignalR-Chat-Application-For-Tracking-Online-Offline-Users-Seen-Not-Seen-and-Chat-History-