🤔What is SignalR?
🤔Why SignalR?
- Hubs: SignalR Hubs provide a high-level API for real-time communication. They abstract away the low-level details of different transport mechanisms and make it easier to broadcast messages to all connected clients or targeted groups of clients. Hubs are defined on the server and can be accessed from the client-side code.
- Clients: Clients refer to the connected devices or applications that communicate with the server using SignalR. In web applications, clients are typically web browsers, but SignalR also supports other platforms like mobile apps and desktop applications.
- Connection: A connection represents a persistent link between a client and the server. SignalR manages these connections transparently, and clients can send and receive messages over these connections.
- Groups: Groups in SignalR allow you to organize clients into logical collections, making it easy to broadcast messages to specific subsets of clients. Clients can be dynamically added or removed from groups based on application logic.
- Set up SignalR: Install the SignalR package in your .NET application and configure it to use the desired transport mechanism (e.g., WebSocket).
- Define SignalR Hub: Create a SignalR Hub on the server that handles the real-time notifications. This hub will have methods to send notifications to specific clients or groups.
- Establish Connection: When a user logs in or opens the application, the client-side code establishes a connection to the SignalR Hub.
- Subscribe to Events: Once the connection is established, the client can subscribe to specific events (e.g., "MessageReceived," "PostLiked") to receive notifications related to those events.
- Server Broadcast: When a new message or a like action occurs, the server-side code invokes the relevant method on the SignalR Hub, passing the appropriate data as parameters.
- Client Notification: The SignalR Hub receives the data and broadcasts it to all connected clients or specific groups. Clients that have subscribed to the relevant events will receive the notifications in real-time.
- Handle Client Actions: When a client receives a notification, the client-side code can update the user interface, display a notification popup, or take any other action based on the nature of the notification.
🧑🏻💻Let's Implement
Prerequisites
- Basic dotnet and entity framework experience
- Basic Angular experience
- Code editor like VSCode
git clone https://github.com/nishanc/AngularDotnetAuthDemo.git
If you run the application now you can register user, login using that user and add a todo item.
Our goal is not this, and to be honest you don't need all of these to implement SignalR, I went ahead and added these extra functionality so that we have a starting point.
Notifications
Our goal is to implement notifications, so let's start. First we need several packages to be installed in the client side and server side. [browse commit]
For the Angular application install @microsoft/signalr using
npm i @microsoft/signalr
builder.Services.AddSignalR(); // Add SignalR
builder.Services.AddCors(options => // Update CORS Policy
{
options.AddPolicy(corsPolicy,
b =>
{
b.WithOrigins(
"http://localhost:4200"
)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
.SetIsOriginAllowed((hosts) => true);
});
});
app.MapHub<NotificationHub>("/notification"); // Map SignalR Endpoint
Now we have a little bit of work to do.
Client Side Implementation
- startConnection() is responsible for initiating connection with the Hub that we created
- listenToNotifications() is responsible for listening to any incoming messages which has been sent by the server using "SendNotificationAsync" method.
- sendNotification() is responsible for calling "SendNotificationToUserAsync" method in the Hub. (that method then calls, SendNotificationAsync again. Look at the class NotificationHub)
constructor(private signalRService: SignalRService,
private notificationService: NotificationService) {}
ngOnInit(): void {
this.signalRService.startConnection();
this.signalRService.listenToNotifications((message) => {
this.notificationService.message(message);
});
}
Here when the application starts, we initiate the connection to the server and then start listening for the notifications. Because our listenToNotifications() accepts a callback, we're using that to call the notification service to display the notification when it's received.
Now in the TodoComponent (or wherever you want to invoke server methods to send notifications) inject the SignalRService.
constructor(private http: HttpClient,
private notificationService: NotificationService,
private signalRService: SignalRService) { }
Then let's call sendNotification() when a item is deleted.
deleteTodo(id: number) {
this.http.delete(`${this.apiUrl}/todo/${id}`).subscribe({
next: () => {
this.todos = this.todos.filter(t => t.id !== id);
this.signalRService.sendNotification("Item Deleted Notification from Client");
},
error: (e) => {
this.notificationService.error(`Error occurred, check console`);
console.error(e);
}
});
}
This will call the SendNotificationToUserAsync() method in our server.
Server Side Implementation
private readonly ITodoRepository _repo;
private IHubContext<NotificationHub, INotificationHub> _notificationHub;
public TodoController(ITodoRepository repo, IHubContext<NotificationHub, INotificationHub> notificationHub)
{
_repo = repo;
_notificationHub = notificationHub;
}
Then use it to send notification when we add a Todo item, as follows.[HttpPost]
public async Task<ActionResult> PostTodo(TodoDto todo)
{
var newTodo = new Todo
{
Name = todo.Name
};
await _repo.AddTodo(newTodo); // Send notification when we add a Todo item
await _notificationHub.Clients.All.SendNotificationAsync("Added Item Notification from Server");
return Ok();
}
Now we're good to see this in action. Start the .NET server and Angular application, create two users using Register, one user with Administrator group and another in Normal User group.
Open http://localhost:4200/ in two browser windows and Login with those two users. The try to Add or Remove items.
You might have noticed that even though we delete the Item from one user, the UI is not updated in the other user. Let's fix that.
Update UI
ngOnInit(): void {
this.signalRService.startConnection();
this.signalRService.listenToNotifications((message) => {
this.notificationService.message(message);
this.eventHandlerService.notificationEvent.emit("refresh");
});
}
Now subscribe to this event on TodoComponent, if the event is "refresh" then re-fetch the Todo items from the server.
Send Notifications to a Group
await _notificationHub.Clients.Group(UserGroups.Administrator)
.SendNotificationAsync("Added Item Notification from Server");
Now the PostTodo method should look like this.
Let's register users to a group now, we'll be doing this in the client side by adding a new function to the SignalRService as joinGroupFeed,public joinGroupFeed(groupName: string) {
return new Promise((resolve, reject) => {
this.hubConnection
.invoke("AddToGroupAsync", groupName)
.then(() => {
console.log("Added to group");
return resolve(true);
}, (err: any) => {
console.log(err);
return reject(err);
});
})
}
In the AuthController we need to add user role as a claim, so that we can decode that to get the user's role from the client side, then we can pass it to the method as groupName.
In the Login method of AuthRepository change, (load UserGroup as well when we fetch the User from database)
var user = await _context.Users.FirstOrDefaultAsync(x => x.Username == username);
tovar user = await _context.Users.Include(x => x.UserGroup).FirstOrDefaultAsync(x => x.Username == username);Replace the text with codes
Then add the role to the claims in the Login method in AuthController.
new Claim(ClaimTypes.Role, userFromRepo.UserGroup.GroupName)
Now in the client side, in the login.component.ts when user logs in we can assign the user to a group.
If you run the application now, "Added Item Notification from Server" notification should only be visible to the administrator users.