Core API Real Time Communication with SignalR
Standard HTTP is a one-way conversation — the client asks, the server answers, and the connection closes. For the BookStore API, this means a client cannot know when a new book is added unless it keeps polling the API repeatedly. SignalR solves this by keeping an open connection between the server and the client, allowing the server to push messages to connected clients instantly.
What Is SignalR?
SignalR is a library that adds real-time functionality to web applications. It manages the connection automatically, choosing the best transport method available:
| Transport | Description | Used When |
|---|---|---|
| WebSockets | Full-duplex, persistent TCP connection | Browser and server both support it |
| Server-Sent Events | One-way stream from server to client | WebSockets unavailable |
| Long Polling | Client keeps a request open until server responds | Fallback for older browsers |
SignalR selects WebSockets automatically if both sides support it — the best option for lowest latency.
Use Cases for SignalR in BookStore API
- Notify all connected clients when a new book is added
- Notify when a book is updated or marked unavailable
- Live admin dashboard showing real-time book inventory counts
Step 1 – Install the SignalR Client Package
Server-side SignalR is already included in ASP.NET Core — no extra package needed on the server. The JavaScript client library is needed only in browser-based frontends.
// Server: no extra package needed
// JavaScript client (if using npm):
npm install @microsoft/signalr
// Or via CDN in HTML:
// <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/7.0.0/signalr.min.js"></script>
Step 2 – Create the Hub
A Hub is the central class in SignalR. Clients connect to a hub, and the hub can send messages to individual clients, groups, or all connected clients.
// Hubs/BookStoreHub.cs
using Microsoft.AspNetCore.SignalR;
namespace BookStoreAPI.Hubs
{
public class BookStoreHub : Hub
{
private readonly ILogger<BookStoreHub> _logger;
public BookStoreHub(ILogger<BookStoreHub> logger)
{
_logger = logger;
}
// Called when a client connects
public override async Task OnConnectedAsync()
{
_logger.LogInformation("Client connected: {ConnectionId}", Context.ConnectionId);
await base.OnConnectedAsync();
}
// Called when a client disconnects
public override async Task OnDisconnectedAsync(Exception? exception)
{
_logger.LogInformation("Client disconnected: {ConnectionId}", Context.ConnectionId);
await base.OnDisconnectedAsync(exception);
}
// Clients can join a group (e.g., a specific category channel)
public async Task JoinCategory(string category)
{
await Groups.AddToGroupAsync(Context.ConnectionId, category);
_logger.LogInformation("Client {ConnectionId} joined group: {Category}",
Context.ConnectionId, category);
}
// Clients can leave a group
public async Task LeaveCategory(string category)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, category);
}
}
}
Step 3 – Register SignalR in Program.cs
// Program.cs
builder.Services.AddSignalR();
var app = builder.Build();
// ... other middleware ...
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
// Map the hub to a URL
app.MapHub<BookStoreHub>("/hubs/bookstore");
app.Run();
Step 4 – Send Notifications from the Book Service
When a book is created, updated, or deleted, the service uses IHubContext to push a message to all connected clients:
// Services/BookService.cs
using Microsoft.AspNetCore.SignalR;
using BookStoreAPI.Hubs;
public class BookService : IBookService
{
private readonly IBookRepository _bookRepository;
private readonly IHubContext<BookStoreHub> _hubContext;
private readonly ILogger<BookService> _logger;
public BookService(
IBookRepository bookRepository,
IHubContext<BookStoreHub> hubContext,
ILogger<BookService> logger)
{
_bookRepository = bookRepository;
_hubContext = hubContext;
_logger = logger;
}
public async Task<Book> CreateAsync(Book book)
{
book.CreatedDate = DateTime.UtcNow;
await _bookRepository.AddAsync(book);
await _bookRepository.SaveAsync();
// Notify all connected clients about the new book
await _hubContext.Clients.All.SendAsync("BookAdded", new
{
book.Id,
book.Title,
book.Author,
book.Price,
book.Category,
book.IsAvailable
});
_logger.LogInformation("Book created and clients notified: {Title}", book.Title);
return book;
}
public async Task<bool> UpdateAsync(int id, Book updatedBook)
{
var book = await _bookRepository.GetByIdAsync(id);
if (book == null) return false;
book.Title = updatedBook.Title;
book.Author = updatedBook.Author;
book.Price = updatedBook.Price;
book.Category = updatedBook.Category;
book.IsAvailable = updatedBook.IsAvailable;
await _bookRepository.UpdateAsync(book);
await _bookRepository.SaveAsync();
// Notify the specific category group
await _hubContext.Clients.Group(book.Category).SendAsync("BookUpdated", new
{
book.Id,
book.Title,
book.Price,
book.IsAvailable
});
return true;
}
public async Task<bool> DeleteAsync(int id)
{
var book = await _bookRepository.GetByIdAsync(id);
if (book == null) return false;
await _bookRepository.DeleteAsync(book);
await _bookRepository.SaveAsync();
// Notify all clients that a book was removed
await _hubContext.Clients.All.SendAsync("BookRemoved", new { BookId = id });
return true;
}
}
SignalR Sending Options
| Method | Who Receives It |
|---|---|
Clients.All.SendAsync() | Every connected client |
Clients.Caller.SendAsync() | Only the client that triggered the method |
Clients.Others.SendAsync() | All clients except the caller |
Clients.Group("name") | All clients in a specific group |
Clients.Client(connectionId) | One specific client |
Clients.User(userId) | All connections of one user |
Step 5 – Connect from a JavaScript Client
<!-- Simple HTML example to listen for BookStore events -->
<script src="signalr.min.js"></script>
<script>
const connection = new signalR.HubConnectionBuilder()
.withUrl("https://localhost:7001/hubs/bookstore")
.withAutomaticReconnect()
.build();
// Listen for new book added event
connection.on("BookAdded", function(book) {
console.log("New book added:", book.title, "by", book.author);
// Update the UI to show the new book
});
// Listen for book updated event
connection.on("BookUpdated", function(book) {
console.log("Book updated:", book.id, "Price:", book.price);
});
// Listen for book removed event
connection.on("BookRemoved", function(data) {
console.log("Book removed with Id:", data.bookId);
});
// Start the connection
connection.start()
.then(() => {
console.log("Connected to BookStore Hub!");
// Join the Technology category group
connection.invoke("JoinCategory", "Technology");
})
.catch(err => console.error("Connection failed:", err));
</script>
Real-Time Flow for BookStore API
Admin sends: POST /api/v2/books (add "Refactoring" by Martin Fowler)
|
v
BookService.CreateAsync()
|
├── Saves book to SQL Server
|
└── _hubContext.Clients.All.SendAsync("BookAdded", bookData)
|
┌──────────────────────┼─────────────────────┐
↓ ↓ ↓
Client A (browser) Client B (mobile) Client C (admin dashboard)
"BookAdded" event "BookAdded" event "BookAdded" event
UI updates instantly App shows new book Dashboard refreshes count
Key Points
- SignalR enables the server to push messages to clients without the client needing to ask — unlike standard HTTP request-response.
- A Hub is the central class — clients connect to it and the server sends messages through it.
IHubContext<T>is injected into services to send messages from outside the Hub (e.g., from the BookService after a save).- SignalR supports groups — clients can subscribe to channels like "Technology" and receive only relevant updates.
- SignalR automatically selects WebSockets → Server-Sent Events → Long Polling as fallback transports.
