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:
- Client 1 sends a message data to Web Application via a 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 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 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.
{
.
.
"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 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 log in 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 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.
Edward 3 years ago
How about when the client app is Angular?
[email protected] 4 years ago
As ABP also get kafka support, could we add the Kafka config session accordingly ?
yekalkan 4 years ago
Hi, You can see "Distributed Event Bus Kafka Integration" document to use Kafka instead of RabbitMQ. https://github.com/abpframework/abp/blob/dev/docs/en/Distributed-Event-Bus-Kafka-Integration.md