In our previous article, we discussed how to implement SignalR for one-to-one chat in an ASP .NET Core Web API. In this post, we will implement the same concept but in an MVC project, and we will also use a database to save the conversation.
In this tutorial, we will implement how to build a one-to-one chat application in ASP.NET MVC using C#.
Let me tell you that SignalR is a web-based real-time bidirectional communication framework and is very useful when we want real-time communication.SignalR is a very important technology when it comes to developing a chat application, for sending notification popups, or if we want to broadcast any real time message.
In this post will Create a one-to-one chat application in ASP.NET MVC using C# with a database involves several steps. In this post will Create a one-to-one chat application in ASP.NET MVC using C# with a database for that we need follow several steps.
Database Design
Creating ASP.NET MVC Setup:
Authentication and Authorization:
Authentication and Authorization is important for any application to secure the application, so in this step we are going to configure user authentication and authorization for we will use ASP.NET Identity so that users able to register, log in, and see users. for chat.
Creating Chat Interface:
It is obvious that we will need a UI interface to chat, so in this part we will create a chat interface where users can view and send messages, and for that, we are going to use HTML, CSS, and JavaScript to create UI.
Database Integration:
To maintain user chat history, a database will also be required. In this step, we will configure our database in our ASP.NET MVC application for storing users, conversations, and messages.
Server-Side Logic:
sending and receiving messages we will have to write logic, so in this step, we will implement server-side logic to handle sending and receiving messages and for that we will use SignalR for real-time communication between clients and the server.
Message Storage:
As we have discussed in the above post, we will store the messages in the database, so in this step, we will write logic to store messages in our database, with the appropriate users and conversations.
Testing and Debugging:
So this is our last and final step, In this step, we will test our application to ensure that messages are sent and received correctly at user end.
So let's start implementation, open your Visual Studio and create a .NET Core MVC project.Open Visual Studio and click on 'Create a new Project' and then select 'ASP.NET Web App with MVC' option
and click on the 'Next' button ,Give the project a name and click on 'Create project' that will create an MVC project for you with basic setup.
I have also provided a download link for the source code at the endpoint post.
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Install-Package Microsoft.EntityFrameworkCore.Tools
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
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=ChatDbV2;User ID=sa;Password=adk@1234;Encrypt=false;");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
if (modelBuilder == null)
throw new ArgumentNullException("modelBuilder");
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
entityType.SetTableName(entityType.DisplayName());
entityType.GetForeignKeys()
.Where(fk => !fk.IsOwnership && fk.DeleteBehavior == DeleteBehavior.Cascade)
.ToList()
.ForEach(fk => fk.DeleteBehavior = DeleteBehavior.Restrict);
}
base.OnModelCreating(modelBuilder);
}
}
public class User
{
public Guid UserId { get; set; }
public string FullName { get; set; }
public string Email { get; set; }
public string Password { 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 DateTime CreatedAt { get; set; }
}
}
CREATE TABLE User (
UserId UNIQUEIDENTIFIER PRIMARY KEY,
FullName NVARCHAR(MAX),
Email NVARCHAR(MAX),
Password NVARCHAR(MAX)
);
INSERT INTO [User] (UserId, FullName, Email, Password) VALUES
('e7f2b6e9-72ab-4a9e-a609-dfc0d20c8efc', 'Rahul Sharma', '[email protected]', '123456'),
('fae92718-941d-4a1f-8f7e-103be7e5cf36', 'Priya Patel', '[email protected]', '654321'),
('6d56dc2b-015d-4f12-968c-4d4db8e4d314', 'Amit Singh', '[email protected]', '987654');
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.SignalR;
using System.Diagnostics;
using static System.Runtime.InteropServices.JavaScript.JSType;
using System.Reflection.Metadata;
using System.Xml.Linq;
using System.Xml;
using AspCoreMvcSingnalR.DatabaseEntity;
using Microsoft.EntityFrameworkCore;
namespace AspCoreMvcSingnalR.SignalRHub
{
public class RealTimeChatHub:Hub
{
public override Task OnDisconnectedAsync(Exception exception)
{
Debug.WriteLine("Client disconnected: " + Context.ConnectionId);
return base.OnDisconnectedAsync(exception);
}
public override Task OnConnectedAsync()
{
return 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);
//Adds the client associated with the current connection ID to a specified group.In this case, we are taking `userId` as a parameter for this function,
//Where `userId` represents the name of the group for loggedin user.
//For each user in our database, we create a unique group with ConnectionId
//This function is used to add a client to a specific chat group, enabling them to participate in conversations within that group.
//We have written this logic, to ensuring that each user only gets messages for their group
//identified by `userId`. that means logged-in users will only receive messages
//sent to their groups, enhancing privacy and ensuring that users do not receive messages for other users to make sure private chat.
}
//Send message to user Group
public async Task SendMessageToUserGroup(string senderUserId, string senderName, string receiverUserId, string message)
{
//Insert message to database then send it to the Client
var optionsBuilder = new DbContextOptionsBuilder<ChatDbContext>();
var _chatDbContext = new ChatDbContext();
UserChatHistory chatHistory = new UserChatHistory();
chatHistory.Message = message;
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", senderUserId, senderName, message);
//"Send the message to all users in the specified group. We take the sender's user ID and the receiver's user ID, and then send the message to
//the group of the receiver's user ID to ensure that only users within the specified receiver group receive the message.
//This allows for private communication between users in a one-to-one chat system."
}
}
}
using AspCoreMvcSingnalR.DatabaseEntity;
using AspCoreMvcSingnalR.SignalRHub;
using Microsoft.AspNetCore.Authentication.Cookies;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
//AddAuthentication for login & in case of unauhroize user clear auth cookis
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(x => x.LoginPath = "/Home/Index");
builder.Services.AddHttpContextAccessor();
//AddSignalR & Hub class
builder.Services.AddSignalR();
builder.Services.AddSingleton<RealTimeChatHub>();
//Add DbContext
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();
Add-Migration InitialCreate
Update-Database
First command will create migrations for us and second will create the database object for us.HomeController.cs
using AspCoreMvcSingnalR.DatabaseEntity;
using AspCoreMvcSingnalR.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
using System.Security.Claims;
namespace AspCoreMvcSingnalR.Controllers
{
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.NameIdentifier, 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);
}
}
public IActionResult Privacy()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}
LoginViewModel.cs
public class LoginViewModel
{
[Required]
public string Email { get; set; }
[Required]
public string Password { get; set; }
}
Index.cshtml
@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>
As you can see in the controller logic, we have created an action method named 'Login' which handles using AspCoreMvcSingnalR.DatabaseEntity;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
namespace AspCoreMvcSingnalR.Controllers
{
[Authorize]
public class UserChatController : Controller
{
ChatDbContext _chatDbContext;
public UserChatController(ChatDbContext chatDbContext)
{
_chatDbContext = chatDbContext;
}
public IActionResult Chat()
{
UserChatViewModel userChatViewModel = new UserChatViewModel();
//Get logged in user detail from Claim
Guid userId = new Guid(User.FindFirstValue(ClaimTypes.NameIdentifier));
string name = User.FindFirstValue(ClaimTypes.Name);
userChatViewModel.LoggedInUser = new User { UserId = userId, FullName = name };
//Get usera for chat exclude logged In user from the list
userChatViewModel.Users = _chatDbContext.Users.Where(a => a.UserId != userId).ToList();
return View(userChatViewModel);
}
public ActionResult GetChatCobversion(Guid userIdToLoadChat)
{
Guid loginUserId = new Guid(User.FindFirstValue(ClaimTypes.NameIdentifier));
var chatHistories = _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", chatHistories);
}
}
public class UserChatViewModel
{
public User LoggedInUser { get; set; }
public List<User> Users { get; set; }// Users avialable for Chat
}
}
@model AspCoreMvcSingnalR.Controllers.UserChatViewModel
@{
ViewData["Title"] = "Chat";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<style type="text/css">
.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: 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.LoggedInUser.FullName</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-userid="" id="chatlist">
</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">
@if(Model!=null && Model.Users.Any())
{
foreach(var user in Model.Users)
{
<div class="user" data-user-id="@user.UserId">
<i class="fa fa-user-circle-o" aria-hidden="true"></i> @user.FullName
</div>
}
}
</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 () {
$(".user").click(function () {
var userId = $(this).data("user-id");
var userName = $(this).text();
$("#chat-user").text(userName);
$("#chat-window").slideDown();
$("#chatlist").attr("data-userid", userId);
$("#chatlist").empty();
LoadUserChatList(userId);
});
$("#close-chat").click(function () {
$("#chat-window").slideUp();
});
});
var senderChatId = "@Model.LoggedInUser.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("ReceiveMessage", async (senderId, senderName,message) => {
var messageBuilder = "<li class='left'><div class='text'><div class='user'>" + GetUserNameWithIcon(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-userid");
var senderName = '@Model.LoggedInUser.FullName';
await connection.invoke("SendMessageToUserGroup", 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 LoadUserChatList(name)
{
Loader(true)
$.ajax({
url: '/UserChat/GetChatCobversion?userIdToLoadChat=' + name,
type: 'GET',
success: function (result) {
$('#chatlist').html(result);
Loader(false)
},
error: function (xhr, status, error) {
console.log(xhr.responseText);
Loader(false)
}
});
}
</script>
@model IEnumerable<AspCoreMvcSingnalR.DatabaseEntity.UserChatHistory>
@{
string loginUserId = ViewData["loginUserId"].ToString();
}
@if (Model.Any())
{
foreach (var chat in Model)
{
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>
</div>
<small>@chat.Message</small>
</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>
</div>
<small>@chat.Message</small>
</li>
}
}
}
You can download the source code from GitHub: https://github.com/ramkumar457/SignalRInAspNetMVC/"