Build a chat app using ASP.NET
A basic understanding of C# and jQuery is needed to follow this tutorial.
Communication in our current age is largely digital, and the most popular form of digital communication is Instant Messaging.
Some applications include some form of chat implementation e.g. Slack or Facebook. In this tutorial, we will consider how to build a chat application using C# .NET.
To follow along with this tutorial, you will require:
- Visual Studio, an IDE popularly used for building .NET projects. View installation details here.
- Basic knowledge of C#.
- Basic knowledge of .NET MVC.
- Basic knowledge of JavaScript (jQuery).
Setting up our chat project
Using our Visual Studio IDE, we’ll create our chat project by following the New Project wizard.
We will:
- Set C# as our language to use.
- Select .NET MVC Project as the template.
- Fill in the Project name e.g. HeyChat.
- Fill in the Solution name i.e. application name.
Creating our chat app
Defining pages and routes
For the purpose of this tutorial, our chat app will consist of 2 pages:
- The front page - where our user signs up.
- The chat view - where our user selects a contact and exchanges messages.
To achieve these views, we will need the following routes:
- The route to render the front page.
- The route to implement login.
- The route to render the chat page.
💡 These routes only render the views and implement user login. We’ll add more routes as we go along.
Adding these routes to our RouteConfig.cs
file we’ll have:
routes.MapRoute(
name: "Home",
url: "",
defaults: new { controller = "Home", action = "Index" }
);
routes.MapRoute(
name: "Login",
url: "login",
defaults: new { controller = "Auth", action = "Login" }
);
routes.MapRoute(
name: "ChatRoom",
url: "chat",
defaults: new { controller = "Chat", action="Index"}
);
These route definitions specify the route pattern and the Controller and Action to handle it.
💡 Creating our project with Visual Studio automatically creates the
HomeContoller.cs
file with anIndex
action. We will use this for our home route.
In our HomeController.cs
we’ll render the front page where our users can log in with:
//HomeController.cs
// ...
Using System.Web.Mvc;
// ...
public class HomeController : Controller
{
public ActionResult Index()
{
if ( Session["user"] != null ) {
return Redirect("/chat");
}
return View();
}
}
💡 The
View
function creates a view response which we return. When it is invoked, C# looks for the default view of the calling controller class. This default view is theindex.cshtml
file found in the Views directory, in a directory with the same name as the Controller i.e. The default view of the HomeController class will be theViews/Home/index.cshtml
file.
Setting up our database
In order to implement our login feature, we’ll need a database to store users. There are several database drivers to choose from but, in this tutorial, we’ll use the MySQL database driver along with a .NET ORM called Entity Framework.
We will start by installing the MySql.Data.Entities
package via NuGet (.NET’s package manager). And then, we’ll install the Entity Framework package also via NuGet, to provide us with our ORM functionality.
💡 To install packages using NuGet, right-click the Packages folder in our project solution; select the
Add Package
option; and search and select your desired package.
Once our packages have been installed, we will begin setting up our database connection and communication.
First, we will add our database connection credentials to the Web.config
file found in our solution folder. In Web.config
we will add:
<connectionStrings>
<add name="YourConnectionName" connectionString="Server=localhost;Database=database_name;Uid=root;Pwd=YourPassword;" providerName="MySql.Data.MySqlClient" />
</connectionStrings>
⚠️ You will need to replace the placeholder values in the snippet above with actual values database values.
The Web.config
file is an XML file and the above connectionStrings
element will be added in the body of the configuration
element of the file.
Next, we’ll create a Models
folder inside our solution folder (on the same folder level as Controllers
). In this folder, we will create our model class - this class is a representation of our table. For the login feature we will create the User.cs
file. In this class file, we will add the properties of our model:
// File: User.cs file
using System;
using System.Collections.Generic;
namespace HeyChat.Models
{
public class User
{
public User()
{
}
public int id { get; set; }
public string name { get; set; }
public DateTime created_at { get; set; }
}
}
💡 To create a model class, right-click the Model folder, select the
Add
andNew File
options, and thenEmpty Class
option filling in the class name.
Our User
model defines an ID for unique identification, user’s name and created date of the user for our users table.
Finally, we will add our database context class. This class reads in the database connection configuration we defined in the Web.config
file and takes the Model classes (Datasets) to which it should apply the configuration.
We will create our context class in our Models
folder, following the same steps of creating a new empty class, and we will name it ChatContext.cs
. In it, we will add the following:
// File: ChatContext.cs
using System;
using System.Data.Entity;
namespace HeyChat.Models
{
public class ChatContext: DbContext
{
public ChatContext() : base("YourConnectionName")
{
}
public static ChatContext Create()
{
return new ChatContext();
}
public DbSet<User> Users { get; set; }
}
}
💡 We are implementing the Entity Framework ORM using the Code First method. This method involves writing the code defining our models (tables) without any existing database or tables. With this method, the database and tables will be created when our application code is executed.
Logging in our users
Since our database connection and model (though as we go along more models may be introduced) have been created, we can proceed with our login functionality.
The front page rendered from the HomeController
will consist of a form that accepts a user’s name. This form will be submitted to the /``login
route which we defined earlier. Following our route definition, this request will be handled by the AuthController
and its Login
action method.
We will create the AuthController
class and add our code for storing or retrieving a user’s details. The option to either store or retrieve will be based on if the user’s name already exists in our Users
Table. The code for the AuthController
is below:
// File: AuthController
// ...
using HeyChat.Models;
public class AuthController : Controller
{
[HttpPost]
public ActionResult Login()
{
string user_name = Request.Form["username"];
if (user_name.Trim() == "") {
return Redirect("/");
}
using (var db = new Models.ChatContext()) {
User user = db.Users.FirstOrDefault(u => u.name == user_name);
if (user == null) {
user = new User { name = user_name };
db.Users.Add(user);
db.SaveChanges();
}
Session["user"] = user;
}
return Redirect("/chat");
}
}
In the code above, we check if a user exists using the name. If it exists we retrieve the user’s details and, if it doesn’t, we create a new record first. Then we assign the user’s details into a session
object for use throughout the application. Lastly, we redirect the user to the chat page.
Rendering the chat page
One feature of most Chat applications is the ability to choose who to chat with. For the purpose of this tutorial, we will assume all registered users can chat with each other so our chat page will offer the possibility of chatting with any of the users stored in our database.
Earlier, we defined our chat route and assigned it to the ChatController
class and its Index
action method.
Let’s create the ChatController
and implement the rendering of the chat page with available contacts. Paste the code below into the ChatController
:
// File: ChatController
// ...
using HeyChat.Models;
namespace HeyChat.Controllers
{
public class ChatController : Controller
{
public ActionResult Index()
{
if (Session["user"] == null) {
return Redirect("/");
}
var currentUser = (Models.User) Session["user"];
using ( var db = new Models.ChatContext() ) {
ViewBag.allUsers = db.Users.Where(u => u.name != currentUser.name )
.ToList();
}
ViewBag.currentUser = currentUser;
return View ();
}
}
}
To get the available contacts, we read all the users in our database except the current user. These users are passed to our client side using ViewBag
. We also pass the current user using ViewBag
.
Now that we have retrieved all the available contacts into the ViewBag
object, we will create the markup for displaying these contacts and the rest of the chat page to the user. To create the view file for our chat page, we create a Chat
folder in the Views
folder.
Next, right click the Chat
folder, select the options to Add
→ Views
, select the Razor template engine and name the file index.cshtml
. Paste in the code below into the file:
<!DOCTYPE html>
<html>
<head>
<title>pChat — Private Chatroom</title>
<link rel="stylesheet" href="@Url.Content("~/Content/app.css")">
</head>
<body>
<!-- Navigation Bar -->
<nav class="navbar navbar-inverse">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#">pChat - @ViewBag.currentUser.name </a>
</div>
<ul class="nav navbar-nav navbar-right">
<li><a href="#">Log Out</a></li>
</ul>
</div>
</nav>
<!-- / Navigation Bar -->
<div class="container">
<div class="row">
<div class="col-xs-12 col-md-3">
<aside class="main visible-md visible-lg">
<div class="row">
<div class="col-xs-12">
<div class="panel panel-default users__bar">
<div class="panel-heading users__heading">
Contacts (@ViewBag.allUsers.Count)
</div>
<div class="__no__chat__">
<p>Select a contact to chat with</p>
</div>
<div class="panel-body users__body">
<ul id="contacts" class="list-group">
@foreach( var user in @ViewBag.allUsers ) {
<a class="user__item contact-@user.id" href="#" data-contact-id="@user.id" data-contact-name="@user.name">
<li>
<div class="avatar">
<img src="@Url.Content("~/Content/no_avatar.png")">
</div>
<span>@user.name</span>
<div class="status-bar"></div>
</li>
</a>
}
</ul>
</div>
</div>
</div>
</div>
</aside>
</div>
<div class="col-xs-12 col-md-9 chat__body">
<div class="row">
<div class="col-xs-12">
<ul class="list-group chat__main">
</ul>
</div>
<div class="chat__type__body">
<div class="chat__type">
<textarea id="msg_box" placeholder="Type your message"></textarea>
<button class="btn btn-primary" id="sendMessage">Send</button>
</div>
</div>
<div class="chat__typing">
<span id="typerDisplay"></span>
</div>
</div>
</div>
</div>
</div>
<script src="@Url.Content("~/Content/app.js")"></script>
</body>
</html>
💡
@Url.Content("~/Content/app.css")
and@Url.Content("~/Content/app.js")
load some previously bundled JavaScript and CSS dependencies such as jQuery and Bootstrap from ourContent
folder.
In our view file, we create a sidebar and loop through the users passed to ViewBag
to indicate the contacts available using Razor’s @foreach
directive. We also add a text area to type and send messages to these contacts.
Selecting contacts and sending messages
When our user selects a contact to chat with, we would like to retrieve the previous messages between the user and the selected contact. In order to achieve this, we would need a table for storing messages between users and a Model for this table.
Let’s create a model called Conversations
in the Models
folder. It will consist of a unique id
, sender_id
, receiver_id
, message
, status
and the created_at
date. The code for the model is below:
// File: Conversation.cs
using System;
namespace HeyChat.Models
{
public class Conversation
{
public Conversation()
{
status = messageStatus.Sent;
}
public enum messageStatus
{
Sent,
Delivered
}
public int id { get; set; }
public int sender_id { get; set; }
public int receiver_id { get; set; }
public string message { get; set; }
public messageStatus status { get; set; }
public DateTime created_at { get; set; }
}
}
After creating the Conversation
model, we will add it to the ChatContext
file as seen below:
// File: ChatContext.cs
using System;
using System.Data.Entity;
namespace HeyChat.Models
{
public class ChatContext: DbContext
{
public ChatContext() : base("MySqlConnection")
{
}
public static ChatContext Create()
{
return new ChatContext();
}
public DbSet<User> Users { get; set; }
public DbSet<Conversation> Conversations { get; set; }
}
}
To retrieve the messages, we will create a route for /contact``/conversations/{contact}
. This route will accept a contact ID, retrieve messages between the current user and the contact, then return the messages in a JSON response.
It will be handled by the ChatController
in the ConversationWithContact
action method as seen below:
//ChatController.cs
...
public JsonResult ConversationWithContact(int contact)
{
if (Session["user"] == null)
{
return Json(new { status = "error", message = "User is not logged in" });
}
var currentUser = (Models.User)Session["user"];
var conversations = new List<Models.Conversation>();
using (var db = new Models.ChatContext())
{
conversations = db.Conversations.
Where(c => (c.receiver_id == currentUser.id
&& c.sender_id == contact) ||
(c.receiver_id == contact
&& c.sender_id == currentUser.id))
.OrderBy(c => c.created_at)
.ToList();
}
return Json(
new { status = "success", data = conversations },
JsonRequestBehavior.AllowGet
);
}
Now that we have a route to retrieve old messages, we will use some jQuery to select the user, fetch the messages and display them on our page.
In our view file, we will create a script
tag to hold our JavaScript and jQuery functions. In it, we’ll add:
...
<script>
let currentContact = null; // Holds current contact
let newMessageTpl =
`<div>
<div id="msg-{{id}}" class="row __chat__par__">
<div class="__chat__">
<p>{{body}}</p>
<p class="delivery-status">Delivered</p>
</div>
</div>
</div>`;
...
// select contact to chat with
$('.user__item').click( function(e) {
e.preventDefault();
currentContact = {
id: $(this).data('contact-id'),
name: $(this).data('contact-name'),
};
$('#contacts').find('li').removeClass('active');
$('#contacts .contact-' + currentContact.id).find('li').addClass('active');
getChat(currentContact.id);
});
// get chat data
function getChat( contact_id ) {
$.get("/contact/conversations/" + contact_id )
.done( function(resp) {
var chat_data = resp.data || [];
loadChat( chat_data );
});
}
//load chat data into view
function loadChat( chat_data ) {
chat_data.forEach( function(data) {
displayMessage(data);
});
$('.chat__body').show();
$('.__no__chat__').hide();
}
function displayMessage( message_obj ) {
const msg_id = message_obj.id;
const msg_body = message_obj.message;
let template = $(newMessageTpl).html();
template = template.replace("{{id}}", msg_id);
template = template.replace("{{body}}", msg_body);
template = $(template);
if ( message_obj.sender_id == @ViewBag.currentUser.id ) {
template.find('.__chat__').addClass('from__chat');
} else {
template.find('.__chat__').addClass('receive__chat');
}
if ( message_obj.status == 1 ) {
template.find('.delivery-status').show();
}
$('.chat__main').append(template);
}
Now that selecting a contact retrieves previous messages, we need our user to be able to send new messages. To achieve this, we will create a route that accepts the message being sent and saves it to the database, and then use some jQuery to read the message text from the textarea
field and send to this route.
//RouteConfig.cs
...
routes.MapRoute(
name: "SendMessage",
url: "send_message",
defaults: new { controller = "Chat", action = "SendMessage" }
);
As specified in the RouteConfig
file, this route will be handled by the SendMessage
action method of the ChatController
.
//ChatController.cs
...
[HttpPost]
public JsonResult SendMessage()
{
if (Session["user"] == null)
{
return Json(new { status = "error", message = "User is not logged in" });
}
var currentUser = (User)Session["user"];
string socket_id = Request.Form["socket_id"];
Conversation convo = new Conversation
{
sender_id = currentUser.id,
message = Request.Form["message"],
receiver_id = Convert.ToInt32(Request.Form["contact"])
};
using ( var db = new Models.ChatContext() ) {
db.Conversations.Add(convo);
db.SaveChanges();
}
return Json(convo);
}
Adding realtime functionality
There are several features of a chat application that require realtime functionality, some of which are:
- Receiving messages sent in realtime.
- Being notified of an impending response - the ‘user is typing’ feature.
- Getting message delivery status.
- Instant notification when a contact goes offline or online.
In achieving these features, we will make use of Pusher Channels. To proceed lets head over to the Pusher dashboard and create a Channels app. You can register for free if you haven’t got an account. Fill out the create app form with the information requested. Next, we’ll install the Pusher Server package in our C# code using NuGet.
To achieve some of our stated realtime features, we will need to be able to trigger events on the client side. In order to trigger client events in this application, we will make use of Private Channels.
We will create our private channel when a contact is chosen. This channel will be used to transmit messages between the logged in user and the contact he is sending a message to.
Private channels require an authentication endpoint from our server side code to be available, because when the channel is instantiated Pusher will try to authenticate that the client has valid access to the channel.
The default route for Pusher’s authentication request is /pusher/auth
, so we will create this route and implement the authentication.
First in our RouteConfig.cs
file we will add the route definition:
routes.MapRoute(
name: "PusherAuth",
url: "pusher/auth",
defaults: new { controller = "Auth", action = "AuthForChannel"}
);
Then, as we have defined above, in the AuthController
class file we will create the AuthForChannel
action method and add:
public JsonResult AuthForChannel(string channel_name, string socket_id)
{
if (Session["user"] == null)
{
return Json(new { status = "error", message = "User is not logged in" });
}
var currentUser = (Models.User)Session["user"];
var options = new PusherOptions();
options.Cluster = "PUSHER_APP_CLUSTER";
var pusher = new Pusher(
"PUSHER_APP_ID",
"PUSHER_APP_KEY",
"PUSHER_APP_SECRET", options);
if (channel_name.IndexOf(currentUser.id.ToString()) == -1)
{
return Json(
new { status = "error", message = "User cannot join channel" }
);
}
var auth = pusher.Authenticate(channel_name, socket_id);
return Json(auth);
}
Our authentication endpoint, above, takes the name of the channel and the socket ID of the client, which are sent by Pusher at a connection attempt.
💡 We will name our private channels using the IDs of the participants of the conversation i.e. the sender and receiver. This we will use to restrict the message from being broadcast to other users of the Messenger app that are not in the specific conversation.
Using the .NET PusherServer
library, we authenticate the user by passing the channel name and socket ID. Then we return the resulting object from authentication via JSON.
For more information on client events and private channels, kindly check out the Pusher documentation.
💡 Client events can only be triggered by private or presence channels.
In the script section of our view, we will instantiate the variable for our private channel. We will also adjust our contact selecting snippet to also create the channel for sending messages, typing and delivery notifications:
...
<script>
...
let currentContact = null; // Holds contact currently being chatted with
let socketId = null;
let currentconversationChannel = null;
let conversationChannelName = null;
//Pusher client side setup
const pusher = new Pusher('PUSHER_APP_ID', {
cluster:'PUSHER_APP_CLUSTER'
});
pusher.connection.bind('connected', function() {
socketId = pusher.connection.socket_id;
});
// select contact to chat with
$('.user__item').click( function(e) {
e.preventDefault();
currentContact = {
id: $(this).data('contact-id'),
name: $(this).data('contact-name'),
};
if ( conversationChannelName ) {
pusher.unsubscribe( conversationChannelName );
}
conversationChannelName = getConvoChannel(
(@ViewBag.currentUser.id * 1) ,
(currentContact.id * 1)
);
currentconversationChannel = pusher.subscribe(conversationChannelName);
bind_client_events();
$('#contacts').find('li').removeClass('active');
$('#contacts .contact-' + currentContact.id).find('li').addClass('active');
getChat(currentContact.id);
});
function getConvoChannel(user_id, contact_id) {
if ( user_id > contact_id ) {
return 'private-chat-' + contact_id + '-' + user_id;
}
return 'private-chat-' + user_id + '-' + contact_id;
}
function bind_client_events(){
//bind private channel events here
currentconversationChannel.bind("new_message", function(msg) {
//add code here
});
currentconversationChannel.bind("message_delivered", function(msg) {
$('#msg-' + msg.id).find('.delivery-status').show();
});
}
We have also saved the socket_id
used to connect to the channel in a variable. This will come in handy later.
Receiving messages sent in realtime
Earlier, we added a route to save messages sent as conversations between the user and a contact.
However, after these messages are saved, we would like the messages to be added to the screen of both the user and contact.
For this to work, in our C# code, after storing the message we will trigger an event via our Pusher private channel. Our clients will then listen to these events and respond to them by adding the messages they carry to the screen.
In our ChatController
class file, after saving the conversation we will add the following:
private Pusher pusher;
//class constructor
public ChatController()
{
var options = new PusherOptions();
options.Cluster = "PUSHER_APP_CLUSTER";
pusher = new Pusher(
"PUSHER_APP_ID",
"PUSHER_APP_KEY",
"PUSHER_APP_SECRET",
options
);
}
[HttpPost]
public JsonResult SendMessage()
{
if (Session["user"] == null)
{
return Json(new { status = "error", message = "User is not logged in" });
}
var currentUser = (User)Session["user"];
string socket_id = Request.Form["socket_id"];
Conversation convo = new Conversation
{
sender_id = currentUser.id,
message = Request.Form["message"],
receiver_id = Convert.ToInt32(Request.Form["contact"])
};
using ( var db = new Models.ChatContext() ) {
db.Conversations.Add(convo);
db.SaveChanges();
}
var conversationChannel = getConvoChannel( currentUser.id, contact);
pusher.TriggerAsync(
conversationChannel,
"new_message",
convo,
new TriggerOptions() { SocketId = socket_id });
return Json(convo);
}
private String getConvoChannel(int user_id, int contact_id)
{
if (user_id > contact_id)
{
return "private-chat-" + contact_id + "-" + user_id;
}
return "private-chat-" + user_id + "-" + contact_id;
}
To make use of the Pusher server-side functionality, we will add using PusherServer;
to the top of our controller file.
💡 We have accepted the
socket_id
from the user when sending the message. This is so that we can specify that the sender is exempted from listening to the event they broadcast.
In our view, we will listen to the new_message
event and use this to add the new message to our view.
//index.cshtml
...
<script>
...
//Send button's click event
$('#sendMessage').click( function() {
$.post("/send_message", {
message: $('#msg_box').val(),
contact: currentContact.id,
socket_id: socketId,
}).done( function (data) {
//display the message immediately on the view of the sender
displayMessage(data);
$('#msg_box').val('');
});
});
function bind_client_events(){
//listening to the message_sent event by the message's recipient
currentconversationChannel.bind("new_message", function(msg) {
if ( msg.receiver_id == @ViewBag.currentUser.id ) {
displayMessage(msg);
}
});
}
Implementing the typing indicator feature
This feature makes users aware that the conversation is active and a response is being typed. To achieve it, we will listen to the keyup
event of our message text area and, upon the occurrence of this keyup
event, we will trigger a client event called client-is-typing
.
// index.cshtml
function bind_client_events(){
currentconversationChannel.bind("client-is-typing", function(data) {
if ( data.user_id == currentContact.id &&
data.contact_id == @ViewBag.currentUser.id ) {
$('#typerDisplay').text( currentContact.name + ' is typing...');
$('.chat__typing').fadeIn(100, function() {
$('.chat__type__body').addClass('typing_display__open');
}).delay(1000).fadeOut(300, function(){
$('.chat__type__body').removeClass('typing_display__open');
});
}
});
...
}
//User is typing
var isTypingCallback = function() {
chatChannel.trigger("client-is-typing", {
user_id: @ViewBag.currentUser.id,
contact_id: currentContact.id,
});
};
$('#msg_box').on('keyup',isTypingCallback);
...
Conclusion
We have built a chat application with some of its basic features in C# with the help of jQuery, and have also implemented some of the common realtime features present in chat applications using Pusher Channels.
15 January 2018
by Neo Ighodaro