I am trying to implement SignalR in a way that allows a client to send a request to the server, which may return a very long message. I need the client to be able to cancel the request if the message is too long or if the user no longer wants to wait.
The idea is that the client can send a signal to the server to stop processing the long-running request. However, when I attempt to implement this using CancellationTokenSource
, the cancellation request doesn't seem to work as expected.
Here is the code I have tried so far:
public class ChatHub : Hub
{
private readonly ChatService _chatService;
public ChatHub(ChatService chatService)
{
_chatService = chatService;
}
private CancellationTokenSource _streamingCancellationTokenSource = null;
public async Task SendMessage(string prompt)
{
_streamingCancellationTokenSource = new CancellationTokenSource();
await foreach (var chunk in _chatService.StreamingAsync(cancellationToken))
{
await Clients.Caller.ReceiveMessageStream(chunk);
}
// Save the changes to the database...
}
public async Task StopStreaming()
{
var tokenSource = _streamingCancellationTokenSource;
if (tokenSource is not null && tokenSource.Token.CanBeCanceled)
{
await tokenSource.CancelAsync();
tokenSource.Dispose();
}
}
}
Problem:
When the client calls StopStreaming()
while the server is still streaming data, the StopStreaming
method on the server is never invoked, and the request continues processing. I also tried modifying the HubOptions<ChatHub>
to allow 2 parallel invocations by setting MaximumParallelInvocationsPerClient
to 2, but this didn’t work either.
services.Configure<HubOptions<ChatHub>>(o =>
{
o.MaximumParallelInvocationsPerClient = 2;
});
Desired Behavior:
Ideally, I would like to pass a CancellationToken
from the client (JavaScript) to the server so that the server can cancel the long-running request directly without needing to make another parallel request. Unfortunately, the current approach doesn't seem to work as expected.
Question:
How can I properly cancel a long-running request in SignalR, allowing the client to signal the server to stop processing when needed?
I am trying to implement SignalR in a way that allows a client to send a request to the server, which may return a very long message. I need the client to be able to cancel the request if the message is too long or if the user no longer wants to wait.
The idea is that the client can send a signal to the server to stop processing the long-running request. However, when I attempt to implement this using CancellationTokenSource
, the cancellation request doesn't seem to work as expected.
Here is the code I have tried so far:
public class ChatHub : Hub
{
private readonly ChatService _chatService;
public ChatHub(ChatService chatService)
{
_chatService = chatService;
}
private CancellationTokenSource _streamingCancellationTokenSource = null;
public async Task SendMessage(string prompt)
{
_streamingCancellationTokenSource = new CancellationTokenSource();
await foreach (var chunk in _chatService.StreamingAsync(cancellationToken))
{
await Clients.Caller.ReceiveMessageStream(chunk);
}
// Save the changes to the database...
}
public async Task StopStreaming()
{
var tokenSource = _streamingCancellationTokenSource;
if (tokenSource is not null && tokenSource.Token.CanBeCanceled)
{
await tokenSource.CancelAsync();
tokenSource.Dispose();
}
}
}
Problem:
When the client calls StopStreaming()
while the server is still streaming data, the StopStreaming
method on the server is never invoked, and the request continues processing. I also tried modifying the HubOptions<ChatHub>
to allow 2 parallel invocations by setting MaximumParallelInvocationsPerClient
to 2, but this didn’t work either.
services.Configure<HubOptions<ChatHub>>(o =>
{
o.MaximumParallelInvocationsPerClient = 2;
});
Desired Behavior:
Ideally, I would like to pass a CancellationToken
from the client (JavaScript) to the server so that the server can cancel the long-running request directly without needing to make another parallel request. Unfortunately, the current approach doesn't seem to work as expected.
Question:
How can I properly cancel a long-running request in SignalR, allowing the client to signal the server to stop processing when needed?
Share edited Mar 4 at 10:55 Jason 22.5k2 gold badges22 silver badges46 bronze badges asked Mar 3 at 20:22 JayJay 2,3102 gold badges30 silver badges71 bronze badges1 Answer
Reset to default 0When we implementing this requirement, we need to pay attention to thread safety and use ConcurrentDictionary
to store token sources to avoid multithreading issues. At the same time, clean up related resources when the Hub is destroyed (OnDisconnectedAsync
) to prevent memory leaks.
Here is the demo for you.
Test Result
Test Code
Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddSignalR();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapHub<ChatHub>("/chatHub");
app.Run();
ChatHub.cs
using Microsoft.AspNetCore.SignalR;
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
public class ChatHub : Hub
{
private static readonly ConcurrentDictionary<string, CancellationTokenSource>
_activeStreams = new();
private readonly ILogger<ChatHub> _logger;
public ChatHub(ILogger<ChatHub> logger)
{
_logger = logger;
}
public async IAsyncEnumerable<string> StreamLongMessage([EnumeratorCancellation] CancellationToken cancellationToken)
{
var speech = "Microsoft Corporation is an American multinational technology conglomerate headquartered in Redmond, Washington.[2] Founded in 1975, the company became highly influential in the rise of personal computers through software like Windows, and the company has since expanded to Internet services, cloud computing, video gaming and other fields. Microsoft is the largest software maker, one of the most valuable public U.S. companies,[a] and one of the most valuable brands globally.\r\n\r\nMicrosoft was founded by Bill Gates and Paul Allen to develop and sell BASIC interpreters for the Altair 8800. It rose to dominate the personal computer operating system market with MS-DOS in the mid-1980s, followed by Windows. During the 41 years from 1980 to 2021 Microsoft released 9 versions of MS-DOS with a median frequency of 2 years, and 13 versions of Windows with a median frequency of 3 years. The company's 1986 initial public offering (IPO) and subsequent rise in its share price created three billionaires and an estimated 12,000 millionaires among Microsoft employees. Since the 1990s, it has increasingly diversified from the operating system market. Steve Ballmer replaced Gates as CEO in 2000. He oversaw the then-largest of Microsoft's corporate acquisitions in Skype Technologies in 2011,[3] and an increased focus on hardware[4][5] that led to its first in-house PC line, the Surface, in 2012, and the formation of Microsoft Mobile through Nokia. Since Satya Nadella took over as CEO in 2014, the company has changed focus towards cloud computing,[6][7] as well as its large acquisition of LinkedIn for $26.2 billion in 2016.[8] Under Nadella's direction, the company has also expanded its video gaming business to support the Xbox brand, establishing the Microsoft Gaming division in 2022 and acquiring Activision Blizzard for $68.7 billion in 2023.[9]\r\n\r\nMicrosoft has been market-dominant in the IBM PC–compatible operating system market and the office software suite market since the 1990s. Its best-known software products are the Windows line of operating systems and the Microsoft Office and Microsoft 365 suite of productivity applications, which most notably include the Word word processor and Excel spreadsheet editor. Its flagship hardware products are the Surface lineup of personal computers and Xbox video game consoles, the latter of which includes the Xbox network; the company also provides a range of consumer Internet services such as Bing web search, the MSN web portal, the Outlook email service and the Microsoft Store. In the enterprise and development fields, Microsoft most notably provides the Azure cloud computing platform, Microsoft SQL Server database software, and Visual Studio.\r\n\r\nMicrosoft is considered one of the Big Five American information technology companies, alongside Alphabet,[b] Amazon, Apple, and Meta.[c] In April 2019, Microsoft reached a trillion-dollar market cap, becoming the third public U.S. company to be valued at over $1 trillion. It has been criticized for its monopolistic practices, and the company's software has been criticized for problems with ease of use, robustness, and security.";
var words = speech.Split(' ');
var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_activeStreams.TryAdd(Context.ConnectionId, cts);
try
{
foreach (var character in words)
{
if (cts.IsCancellationRequested)
{
_logger.LogInformation("Stream cancelled by client");
yield break;
}
yield return character.ToString();
await Task.Delay(100, cts.Token);
}
}
finally
{
_activeStreams.TryRemove(Context.ConnectionId, out _);
cts.Dispose();
}
}
public Task CancelStream()
{
if (_activeStreams.TryGetValue(Context.ConnectionId, out var cts))
{
_logger.LogInformation($"Cancelling stream for {Context.ConnectionId}");
cts.Cancel();
}
return Task.CompletedTask;
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
_activeStreams.TryRemove(Context.ConnectionId, out var cts);
cts?.Dispose();
await base.OnDisconnectedAsync(exception);
}
}
Index.cshtml
@{
ViewData["Title"] = "SignalR Cancellation Demo";
}
<!DOCTYPE html>
<html>
<head>
<title>@ViewData["Title"]</title>
</head>
<body>
<button id="start">Start Stream</button>
<button id="stop">Stop Stream</button>
<div id="output"></div>
<script src="https://cdnjs.cloudflare/ajax/libs/microsoft-signalr/6.0.1/signalr.min.js"></script>
<script>
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.configureLogging(signalR.LogLevel.Information)
.build();
let currentStream = null;
document.getElementById('start').addEventListener('click', async () => {
try {
output.innerHTML = '';
currentStream = connection.stream("StreamLongMessage")
.subscribe({
next: (word) => {
output.innerHTML += word + ' ';
},
complete: () => {
console.log("Stream completed");
},
error: (err) => {
console.error("Stream error:", err);
}
});
} catch (err) {
console.error(err);
}
});
document.getElementById('stop').addEventListener('click', async () => {
if (currentStream) {
currentStream.dispose();
await connection.invoke("CancelStream");
currentStream = null;
}
});
async function start() {
try {
await connection.start();
console.log("Connected to SignalR");
} catch (err) {
console.error(err);
setTimeout(start, 5000);
}
}
start();
</script>
</body>
</html>