Typically, ASP.NET MVC model handles requests by receiving the request, do some processing and then return the response, all within an action method. But in case of doing a long polling, we don’t have anything to return until the expected event occurs. Therefore, the long polling request needs to be able to wait for the event, without holding up the processing thread. There doesn’t seem to have a built-in capability of ASP.NET MVC to perform long polling, but with a simple setup, long polling can be done through ASP.NET MVC.
The little trick is the use of class TaskCompletionSource
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
using System.Linq; using System.Collections.Generic; using System.Threading.Tasks; public class SimpleLongPolling { private static List<SimpleLongPolling> _sSubscribers = new List<SimpleLongPolling>(); public static void Publish(string channel, string message) { lock (_sSubscribers) { var all = _sSubscribers.ToList(); foreach (var poll in all) { if (poll._Channel == channel) poll.Notify(message); } } } private TaskCompletionSource<bool> _TaskCompleteion = new TaskCompletionSource<bool>(); private string _Channel { get; set; } private string _Message { get; set; } public SimpleLongPolling(string channel) { this._Channel = channel; lock (_sSubscribers) { _sSubscribers.Add(this); } } private void Notify(string message) { this._Message = message; this._TaskCompleteion.SetResult(true); } public async Task<string> WaitAsync() { await Task.WhenAny(_TaskCompleteion.Task, Task.Delay(60000)); // blocking wait until event occurs or timeout lock (_sSubscribers) { _sSubscribers.Remove(this); } return this._Message; } } |
Let’s take a look at the highlighted lines. WaitAsync() is invoked when we want to initiate a long polling. Task.WhenAny() will perform a blocking wait till the TaskCompletionSource has it’s result set to ‘True’ by a triggering event. Task.Delay(60000) defines a 60 seconds timeout. Therefore, the blocking will also end if the event has not occured within 60 seconds. _Message property stores the data provided by the triggering event, or null if timeout.
So MVC action to perform a long polling request looks like this:
1 2 3 4 5 6 |
public async Task<IActionResult> DoLongPolling() { SimpleLongPolling lp = new SimpleLongPolling("sample-channel"); var message = await lp.WaitAsync(); return new ObjectResult(new { Message = (message != null ? message : "Long polling timeout!") }); } |
The triggering event, when occurs, shall invoke SimpleLongPolling.Publish() to notify all long polling requests that are waiting on the same channel. Notify() of each matching long polling request will be called to set the TaskCompletionSource result to ‘True’, thus ending the blocking wait and continue execution.
MVC action to simulate an event trigger looks like this:
1 2 3 4 5 |
public IActionResult SimulateTrigger(string message = "Sample publish message") { SimpleLongPolling.Publish("sample-channel", message); return new ObjectResult(new { Message = message, Status = "Published" }); } |
This is a simple implementation to demonstrate how the long polling requests can be handled by ASP.NET and it will work on a single server. To work in multiple hosts (web farm), ability to communicate among different hosts using feature like Redis Pub/Sub or some messaging bus infrastructure would be required.
Note: while there are newer technologies like SignalR using web sockets or Web Push that facilitate real time update/notification from server, there are still occasions when simple long polling is still a reasonable way of offering real time update/notification.
Example code is available at Github.