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 detailshere.
– 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 theNew 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 ourRouteConfig.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 theController andAction 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 ourHomeController.cs
we’ll render the front page where our users can log in with:
//HomeController.cs// ...UsingSystem.Web.Mvc;// ...publicclassHomeController:Controller{publicActionResultIndex(){if(Session["user"]!=null){returnRedirect("/chat");}returnView();}}
💡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 the**index.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 the**Views/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 theMySql.Data.Entities
package via NuGet (.NET’s package manager). And then, we’ll install theEntity ******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 theWeb.config
file found in our solution folder. InWeb.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.
TheWeb.config
file is anXML file and the aboveconnectionStrings
element will be added in the body of theconfiguration
element of the file.
Next, we’ll create aModels
folder inside our solution folder (on the same folder level asControllers
). In this folder, we will create our model class – this class is a representation of our table. For the login feature we will create theUser.cs
file. In this class file, we will add the properties of our model:
// File: User.cs fileusingSystem;usingSystem.Collections.Generic;namespaceHeyChat.Models{publicclassUser{publicUser(){}publicintid{get;set;}publicstringname{get;set;}publicDateTimecreated_at{get;set;}}}
💡To create a model class, right-click the Model folder, select the
**Add**
and**New File**
options, and then**Empty Class**
option filling in the class name.
OurUser
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 theWeb.config
file and takes the Model classes (Datasets) to which it should apply the configuration.
We will create our context class in ourModels
folder, following the same steps of creating a new empty class, and we will name itChatContext.cs
. In it, we will add the following:
// File: ChatContext.csusingSystem;usingSystem.Data.Entity;namespaceHeyChat.Models{publicclassChatContext:DbContext{publicChatContext():base("YourConnectionName"){}publicstaticChatContextCreate(){returnnewChatContext();}publicDbSet<User>Users{get;set;}}}
💡We areimplementing the EntityFramework 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 theHomeController
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 theAuthController
and itsLogin
action method.
We will create theAuthController
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 ourUsers
Table. The code for theAuthController
is below:
// File: AuthController// ...usingHeyChat.Models;publicclassAuthController:Controller{[HttpPost]publicActionResultLogin(){stringuser_name=Request.Form["username"];if(user_name.Trim()==""){returnRedirect("/");}using(vardb=newModels.ChatContext()){Useruser=db.Users.FirstOrDefault(u=>u.name==user_name);if(user==null){user=newUser{name=user_name};db.Users.Add(user);db.SaveChanges();}Session["user"]=user;}returnRedirect("/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 asession
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 theChatController
class and itsIndex
action method.
Let’s create theChatController
and implement the rendering of the chat page with available contacts. Paste the code below into theChatController
:
// File: ChatController// ...usingHeyChat.Models;namespaceHeyChat.Controllers{publicclassChatController:Controller{publicActionResultIndex(){if(Session["user"]==null){returnRedirect("/");}varcurrentUser=(Models.User)Session["user"];using(vardb=newModels.ChatContext()){ViewBag.allUsers=db.Users.Where(u=>u.name!=currentUser.name).ToList();}ViewBag.currentUser=currentUser;returnView();}}}
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 usingViewBag
. We also pass the current user usingViewBag
.
Now that we have retrieved all the available contacts into theViewBag
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 aChat
folder in theViews
folder.
Next, right click theChat
folder, select the options toAdd
→Views
, select the Razor template engine and name the fileindex.cshtml
. Paste in the code below into the file:
<html><head><title>pChat—PrivateChatroom</title><linkrel="stylesheet"href="@Url.Content("~/Content/app.css")"></head><body><navclass="navbarnavbar-inverse"><divclass="container-fluid"><divclass="navbar-header"><aclass="navbar-brand" href="#">pChat - @ViewBag.currentUser.name </a></div><ulclass="navnavbar-navnavbar-right"><li><ahref="#">LogOut</a></li></ul></div></nav><divclass="container"><divclass="row"><divclass="col-xs-12col-md-3"><asideclass="mainvisible-mdvisible-lg"><divclass="row"><divclass="col-xs-12"><divclass="panelpanel-defaultusers__bar"><divclass="panel-headingusers__heading">Contacts(@ViewBag.allUsers.Count)</div><divclass="__no__chat__"><p>Selectacontacttochatwith</p></div><divclass="panel-bodyusers__body"><ulid="contacts"class="list-group">@foreach(varuserin@ViewBag.allUsers){<aclass="user__itemcontact-@user.id" href="#" data-contact-id="@user.id" data-contact-name="@user.name"><li><divclass="avatar"><imgsrc="@Url.Content("~/Content/no_avatar.png")"></div><span>@user.name</span><divclass="status-bar"></div></li></a>}</ul></div></div></div></div></aside></div><divclass="col-xs-12col-md-9chat__body"><divclass="row"><divclass="col-xs-12"><ulclass="list-groupchat__main"></ul></div><divclass="chat__type__body"><divclass="chat__type"><textareaid="msg_box"placeholder="Type your message"></textarea><buttonclass="btnbtn-primary"n">sendMessage">Send</button></div></div><divclass="chat__typing"><spanid="typerDisplay"></span></div></div></div></div></div><scriptsrc="@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 our**Content**
folder.
In our view file, we create a sidebar and loop through the users passed toViewBag
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 calledConversations
in theModels
folder. It will consist of a uniqueid
,sender_id
,receiver_id
,message
,status
and thecreated_at
date. The code for the model is below:
// File: Conversation.csusingSystem;namespaceHeyChat.Models{publicclassConversation{publicConversation(){status=messageStatus.Sent;}publicenummessageStatus{Sent,Delivered}publicintid{get;set;}publicintsender_id{get;set;}publicintreceiver_id{get;set;}publicstringmessage{get;set;}publicmessageStatusstatus{get;set;}publicDateTimecreated_at{get;set;}}}
After creating theConversation
model, we will add it to theChatContext
file as seen below:
// File: ChatContext.csusingSystem;usingSystem.Data.Entity;namespaceHeyChat.Models{publicclassChatContext:DbContext{publicChatContext():base("MySqlConnection"){}publicstaticChatContextCreate(){returnnewChatContext();}publicDbSet<User>Users{get;set;}publicDbSet<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 theChatController
in theConversationWithContact
action method as seen below:
//ChatController.cs...publicJsonResultConversationWithContact(intcontact){if(Session["user"]==null){returnJson(new{status="error",message="User is not logged in"});}varcurrentUser=(Models.User)Session["user"];varconversations=newList<Models.Conversation>();using(vardb=newModels.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();}returnJson(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 ascript
tag to hold our JavaScript and jQuery functions. In it, we’ll add:
...<script>letcurrentContact=null;// Holds current contactletnewMessageTpl=`<div> <div> <div> <p>{{body}}</p> <p>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 datafunctiongetChat(contact_id){$.get("/contact/conversations/"+contact_id).done(function(resp){varchat_data=resp.data||[];loadChat(chat_data);});}//load chat data into viewfunctionloadChat(chat_data){chat_data.forEach(function(data){displayMessage(data);});$('.chat__body').show();$('.__no__chat__').hide();}functiondisplayMessage(message_obj){constmsg_id=message_obj.id;constmsg_body=message_obj.message;lettemplate=$(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 thetextarea
field and send to this route.
//RouteConfig.cs ... routes.MapRoute( name: "SendMessage", url: "send_message", defaults: new { controller = "Chat", action = "SendMessage" } );
As specified in theRouteConfig
file, this route will be handled by theSendMessage
action method of theChatController
.
//ChatController.cs...[HttpPost]publicJsonResultSendMessage(){if(Session["user"]==null){returnJson(new{status="error",message="User is not logged in"});}varcurrentUser=(User)Session["user"];stringsocket_id=Request.Form["socket_id"];Conversationconvo=newConversation{sender_id=currentUser.id,message=Request.Form["message"],receiver_id=Convert.ToInt32(Request.Form["contact"])};using(vardb=newModels.ChatContext()){db.Conversations.Add(convo);db.SaveChanges();}returnJson(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 ofPusher. To proceed lets head over to the Pusherdashboard and create an app. You canregister for free if you haven’t got an account. Fill out the create app form with the information requested. Next, we’ll install thePusher 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 ourRouteConfig.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 theAuthController
class file we will create theAuthForChannel
action method and add:
publicJsonResultAuthForChannel(stringchannel_name,stringsocket_id){if(Session["user"]==null){returnJson(new{status="error",message="User is not logged in"});}varcurrentUser=(Models.User)Session["user"];varoptions=newPusherOptions();options.Cluster="PUSHER_APP_CLUSTER";varpusher=newPusher("PUSHER_APP_ID","PUSHER_APP_KEY","PUSHER_APP_SECRET",options);if(channel_name.IndexOf(currentUser.id.ToString())==-1){returnJson(new{status="error",message="User cannot join channel"});}varauth=pusher.Authenticate(channel_name,socket_id);returnJson(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 .NETPusherServer
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 Pusherdocumentation.
💡 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>...letcurrentContact=null;// Holds contact currently being chatted withletsocketId=null;letcurrentconversationChannel=null;letconversationChannelName=null;//Pusher client side setupconstpusher=newPusher('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);});functiongetConvoChannel(user_id,contact_id){if(user_id>contact_id){return'private-chat-'+contact_id+'-'+user_id;}return'private-chat-'+user_id+'-'+contact_id;}functionbind_client_events(){//bind private channel events herecurrentconversationChannel.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 thesocket_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 ourChatController
class file, after saving the conversation we will add the following:
privatePusherpusher;//class constructorpublicChatController(){varoptions=newPusherOptions();options.Cluster="PUSHER_APP_CLUSTER";pusher=newPusher("PUSHER_APP_ID","PUSHER_APP_KEY","PUSHER_APP_SECRET",options);}[HttpPost]publicJsonResultSendMessage(){if(Session["user"]==null){returnJson(new{status="error",message="User is not logged in"});}varcurrentUser=(User)Session["user"];stringsocket_id=Request.Form["socket_id"];Conversationconvo=newConversation{sender_id=currentUser.id,message=Request.Form["message"],receiver_id=Convert.ToInt32(Request.Form["contact"])};using(vardb=newModels.ChatContext()){db.Conversations.Add(convo);db.SaveChanges();}varconversationChannel=getConvoChannel(currentUser.id,contact);pusher.TriggerAsync(conversationChannel,"new_message",convo,newTriggerOptions(){SocketId=socket_id});returnJson(convo);}privateStringgetConvoChannel(intuser_id,intcontact_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 addusing 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 thenew_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 senderdisplayMessage(data);$('#msg_box').val('');});});functionbind_client_events(){//listening to the message_sent event by the message's recipientcurrentconversationChannel.bind("new_message",function(msg){if(msg.receiver_id==@ViewBag.currentUser.id){displayMessage(msg);}});}
Implementing the ‘user Is Typing’ Feature
This feature makes users aware that the conversation is active and a response is being typed. To achieve it, we will listen to thekeyup
event of our message text area and, upon the occurrence of thiskeyup
event, we will trigger a client event calledclient-is-typing
.
// index.cshtmlfunctionbind_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+'istyping...');$('.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 typingvarisTypingCallback=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. This is just a small portion of what building C# applications with the help of Pusher’s realtime functionality has to offer.
We would love to hear your thoughts and take on building applications with C# and Pusher. If you have any questions on segments of this tutorial, we would love to hear those too. So please, leave a comment below! The entire code from this tutorial is available onGithub.
This post first appeared on thePusher blog.
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse