Real-Time Messaging In A Distributed Architecture Using Abp Framework, SingalR & RabbitMQ
By: Yunus Emre Kalkan
Tags: Messaging, Signalr, Rabbitmq, Aspnetcore, Abp Framework

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 using 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:

1. Client 1 sends message data to Web Application via a REST call.

2. Web Application redirects the message data to Http Api.

3. 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.

4. Web application, that is subscribed to that event, receives it.

5. Web Application sends the message to Client 2.

For this example flow, we could send a 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 the 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 the 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](https://docs.abp.io/en/abp/latest/Application-Services) that publishes the message as an 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 the 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 yarn and gulp commands.

(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 the SignalR connection. We can inherit it from the 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 for 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 the directory where *.HttpApi.Host.csproj file exists, 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 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 a Chat Page

We create the files below in the Pages folder of *.Web Project.

Chat.cshtml:

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 the 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 run, 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 log in with the user we created above. Now we can go to the chat page and start messaging:

We can test with more users. All sent and incoming messages are displayed in the left box.

Source code

The source code of the final application can be found on the GitHub repository.