Real Time Messaging In A Distributed Architecture Using Abp Framework, SignalR & RabbitMQ
In this article, we will build a basic real time messaging application in a distributed architecture. We will use Abp Framework for infrastructure and tiered startup template, SignalR for real time server-client communication and RabbitMQ as the distributed event bus.
When Web & API tiers are separated, it is impossible to directly send a server-to-client message from the HTTP API. This is also true for a microservice architected application. We suggest to use the distributed event bus to deliver the message from API application to the web application, then to the client.
Above, you can see the data-flow that we will implement in this article. This diagram represents how data will flow in our application when Client 1 sends a message to Client 2. It is explained in 5 steps:
- Client 1 sends a message data to Web Application via REST call.
- Web Application redirects the message data to Http Api.
- The message data is processed in Http Api and Http Api publishes an event that holds the data that will be sent to Client 2.
- Web application, that is subscribed to that event, receives it.
- Web Application sends the message to Client 2.
For this example flow, we could send message from Client 1 to Client 2 directly on the SignalR Hub. However, what we are trying here to demonstrate is sending a real-time message from the Http Api to a specific user who is connected to the web application.
Implementation
Startup template and initial run
Abp Framework offers startup templates to get into the business faster. We can download a new tiered startup template using Abp CLI:
abp new SignalRTieredDemo --tiered
After download is finished, we run *.DbMigrator project to create the database and seed initial data (admin user, role etc). Then we run *.IdentityServer, *.HttpApi.Host and *.Web to see our application working.
Creating Application Layer
We create an application service that publishes the message as event.
In *.Application.Contracts project:
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace SignalRTieredDemo
{
public interface IChatAppService : IApplicationService
{
Task SendMessageAsync(SendMessageInput input);
}
}
Input DTO for SendMessageAsync method:
namespace SignalRTieredDemo
{
public class SendMessageInput
{
public string TargetUserName { get; set; }
public string Message { get; set; }
}
}
Event transfer object (ETO) for communication on event bus:
using System;
namespace SignalRTieredDemo
{
public class ReceivedMessageEto
{
public string ReceivedText { get; set; }
public Guid TargetUserId { get; set; }
public string SenderUserName { get; set; }
public ReceivedMessageEto(
Guid targetUserId, string senderUserName, string receivedText)
{
ReceivedText = receivedText;
TargetUserId = targetUserId;
SenderUserName = senderUserName;
}
}
}
In *.Application project:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Identity;
namespace SignalRTieredDemo
{
public class ChatAppService: SignalRTieredDemoAppService, IChatAppService
{
private readonly IIdentityUserRepository _identityUserRepository;
private readonly ILookupNormalizer _lookupNormalizer;
private readonly IDistributedEventBus _distributedEventBus;
public ChatAppService(IIdentityUserRepository identityUserRepository, ILookupNormalizer lookupNormalizer, IDistributedEventBus distributedEventBus)
{
_identityUserRepository = identityUserRepository;
_lookupNormalizer = lookupNormalizer;
_distributedEventBus = distributedEventBus;
}
public async Task SendMessageAsync(SendMessageInput input)
{
var targetId = (await _identityUserRepository.FindByNormalizedUserNameAsync(_lookupNormalizer.NormalizeName(input.TargetUserName))).Id;
await _distributedEventBus.PublishAsync(new ReceivedMessageEto(targetId, CurrentUser.UserName, input.Message));
}
}
}
Creating API Layer
We create an endpoint for sending message that redirects the process to application layer:
In controllers folder of *.HttpApi project:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc;
namespace SignalRTieredDemo.Controllers
{
[Route("api/app/chat")]
public class ChatController : AbpController, IChatAppService
{
private readonly IChatAppService _chatAppService;
public ChatController(IChatAppService chatAppService)
{
_chatAppService = chatAppService;
}
[HttpPost]
[Route("send-message")]
public async Task SendMessageAsync(SendMessageInput input)
{
await _chatAppService.SendMessageAsync(input);
}
}
}
Adding SignalR
To add SignalR to our solution, we add Volo.Abp.AspNetCore.SignalR
nuget package to *.Web project.
And then add AbpAspNetCoreSignalRModule
dependency:
namespace SignalRTieredDemo.Web
{
[DependsOn(
...
typeof(AbpAspNetCoreSignalRModule) // <---
)]
public class SignalRTieredDemoWebModule : AbpModule
{
Also, we need to add @abp/signalr npm package to package.json in *.Web project, then run abp install-libs command.
{
.
.
"dependencies": {
.
.
"@abp/signalr": "^2.9.0"
}
}
Remember to add the latest package version.
You can find more information for Abp SignalR Integration on the related document.
Creating A Hub
We need a hub for SignalR connection. We can inherit it from AbpHup
base class.
In *.Web project:
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.AspNetCore.SignalR;
namespace SignalRTieredDemo.Web
{
[Authorize]
public class ChatHub : AbpHub
{
}
}
While you could inherit from the standard Hub
class, AbpHub
has some common services pre-injected as base properties, which is useful on your development.
Adding & Configuring RabbitMQ
To add RabbitMQ to our solution, we add Volo.Abp.EventBus.RabbitMQ
nuget package to *.HttpApi.Host and *.Web projects.
Launch a command line, navigate to directory where *.HttpApi.Host.csproj file exist, and run the command below using Abp CLI:
abp add-package Volo.Abp.EventBus.RabbitMQ
Then do the same for *.Web project.
After we add the package, we configure RabbitMQ by adding configuration in appsettings.json files of those projects.
For *.HttpApi.Host project:
{
...
"RabbitMQ": {
"Connections": {
"Default": {
"HostName": "localhost"
}
},
"EventBus": {
"ClientName": "SignalRTieredDemo_HttpApi",
"ExchangeName": "SignalRTieredDemoTest"
}
},
...
}
For *.Web project:
{
...
"RabbitMQ": {
"Connections": {
"Default": {
"HostName": "localhost"
}
},
"EventBus": {
"ClientName": "SignalRTieredDemo_Web",
"ExchangeName": "SignalRTieredDemoTest"
}
},
...
}
Handling New Message Event
Once we publish a new message event from Http Api
, we must to handle it in Web Application
. Therefore we need an event handler in *.Web Project:
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
namespace SignalRTieredDemo.Web
{
public class ReceivedMessageEventHandler :
IDistributedEventHandler<ReceivedMessageEto>,
ITransientDependency
{
private readonly IHubContext<ChatHub> _hubContext;
public ReceivedMessageEventHandler(IHubContext<ChatHub> hubContext)
{
_hubContext = hubContext;
}
public async Task HandleEventAsync(ReceivedMessageEto eto)
{
var message = $"{eto.SenderUserName}: {eto.ReceivedText}";
await _hubContext.Clients
.User(eto.TargetUserId.ToString())
.SendAsync("ReceiveMessage", message);
}
}
}
Creating Chat Page
We create the files below in Pages folder of *.Web Project.
Chat.cshtml:
@page
@using Volo.Abp.AspNetCore.Mvc.UI.Packages.SignalR
@model SignalRTieredDemo.Web.Pages.ChatModel
@section styles {
<abp-style src="/Pages/Chat.css" />
}
@section scripts {
<abp-script type="typeof(SignalRBrowserScriptContributor)" />
<abp-script src="/Pages/Chat.js" />
}
<h1>Chat</h1>
<div>
<abp-row>
<abp-column size-md="_6">
<div>All Messages:</div>
<ul id="MessageList" style="">
</ul>
</abp-column>
<abp-column size-md="_6">
<form>
<abp-row>
<abp-column>
<label for="TargetUser">Target user:</label>
<input type="text" id="TargetUser" />
</abp-column>
</abp-row>
<abp-row class="mt-2">
<abp-column>
<label for="Message">Message:</label>
<textarea id="Message" rows="4"></textarea>
</abp-column>
</abp-row>
<abp-row class="mt-2">
<abp-column>
<abp-button type="submit" id="SendMessageButton" button-type="Primary" size="Block" text="SEND!" />
</abp-column>
</abp-row>
</form>
</abp-column>
</abp-row>
</div>
Chat.cshtml.cs:
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace SignalRTieredDemo.Web.Pages
{
public class ChatModel : PageModel
{
public void OnGet()
{
}
}
}
Chat.css:
#MessageList {
border: 1px solid gray;
height: 400px;
overflow: auto;
list-style: none;
padding-left: 0;
padding: 10px;
}
#TargetUser {
width: 100%;
}
#Message {
width: 100%;
}
Chat.js:
$(function () {
var connection = new signalR.HubConnectionBuilder().withUrl("/signalr-hubs/chat").build();
connection.on("ReceiveMessage", function (message) {
console.log(message);
$('#MessageList').append('<li><strong><i class="fas fa-long-arrow-alt-right"></i> ' + message + '</strong></li>');
});
connection.start().then(function () {
}).catch(function (err) {
return console.error(err.toString());
});
$('#SendMessageButton').click(function (e) {
e.preventDefault();
var targetUserName = $('#TargetUser').val();
var message = $('#Message').val();
$('#Message').val('');
signalRTieredDemo.controllers.chat.sendMessage({
targetUserName: targetUserName,
message: message
}).then(function() {
$('#MessageList')
.append('<li><i class="fas fa-long-arrow-alt-left"></i> ' + abp.currentUser.userName + ': ' + message + '</li>');
});
});
});
Then we can add this new page to menu on *MenuContributor.cs in Menus folder:
...
public class SignalRTieredDemoMenuContributor : IMenuContributor
{
...
private Task ConfigureMainMenuAsync(MenuConfigurationContext context)
{
...
context.Menu.Items.Add(new ApplicationMenuItem("SignalRDemo.Chat", "Chat", "/Chat")); // <-- We add this line
return Task.CompletedTask;
}
...
}
Running & Testing
We run *.IdentityServer, *.HttpApi.Host and *.Web in order. After *.Web project is ran, firstly login with admin
username and 1q2w3E*
password.
After we login, go to /Identity/Users
page and create a new user. So that we can chat with them.
Then we open the application in another browser and login with the user we created above. Now we can go to chat page and start messaging:
We can test with more user. All sent and incoming messages are displayed in the left box.
Source code
Source code of the final application can be found on the GitHub repository.
Comments
jonny 164 weeks ago
great
Gökhan Atılgan 147 weeks ago
It is not working. await _chatAppService.SendMessageAsync(input); //(In .HttpApi ChatController) In this line always get internal server error exception. I tried to re-creating project 4 times :) Because the name of route of the Route["send-message"] conflict in swagger. However, if you change the "send-messages" project is working no any exception. But, ReceivedMessageEventHandler not working I put here the breakpoint, never comes here.
kuanlin.albert@gmail.com 143 weeks ago
Hi, It's a great feature. The only thing I wandered is how to delay the handleeventasync in web project after signed in as target user and open the chat dialog. Because it makes sense to receive messages from RabbitMQ which is queued many messages. I am not sure how to handle it. Please help. Thanks.
burakdemir5507@gmail.com 111 weeks ago
How do you do signalr number of connections scaling? I have 80,000 users on my system. I developed the system using abp. Will SignalR be able to actively open 50,000 instant sockets?
Nguyen Van Tuan 6 weeks ago
It is not working, i send a messages and target user client don't receive message.