I have a process that takes a substantial amount of time (several minutes) and would like to trigger it from a web browser and give feedback to the user that it is working. I created some code that uses WebSockets to send progress and messages back to the client.
So my question is:
- Is there a better or more concise way to do this?
- Is there anything I am overlooking with the code that may cause issues?
- Any other suggestions including coding style or best practices?
I know that older browsers don't support WebSockets and am ok not supporting them.
Contents of startup.cs
public class Startup
{
//...
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
//...
app.UseWebSockets(); //from package System.Net.WebSockets.Server
//...
}
}
Content from HomeController.cs
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace WebSocketTest.Controllers
{
public static class WebSocketJsonExtensions
{
/// <summary>
/// Sends an object as a message (after first serializing to JSON)
/// </summary>
public static async Task SendJsonAsync(this WebSocket socket, object value)
{
var serializedJson = JsonConvert.SerializeObject(value);
var messageBytes = Encoding.UTF8.GetBytes(serializedJson);
var arraySegment = new ArraySegment<byte>(messageBytes);
await socket.SendAsync(arraySegment, WebSocketMessageType.Text, true, CancellationToken.None);
}
/// <summary>
/// Closes the web socket sending the reason object as serialized Json
/// </summary>
public static async Task CloseJsonAsync(this WebSocket socket, object reason)
{
var serializedJson = JsonConvert.SerializeObject(reason);
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, serializedJson, CancellationToken.None);
}
}
public class VoidResult : IActionResult
{
public async Task ExecuteResultAsync(ActionContext context)
{
if (!context.HttpContext.Response.HasStarted)
{
await new NoContentResult().ExecuteResultAsync(context);
}
}
}
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
public async Task<IActionResult> SlowTask()
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return this.BadRequest("SlowTask only supports websocket requests");
var socket = await HttpContext.WebSockets.AcceptWebSocketAsync();
var clientChain = Task.CompletedTask;
int max = 20;
for (int i = 0; i < max; i++)
{
//simulated slow task
await Task.Delay(500);
var message = new
{
Message = $"{i + 1}/{max} complete",
ProgressValue = i + 1,
ProgressMax = max
};
//messages are sent without waiting for the client to ensure server process is not delayed
//messages are sent in a chain to preserve order
clientChain = clientChain
.ContinueWith(async (T) => await socket.SendJsonAsync(message))
.Unwrap();
}
//ensures the remainder of the messages are sent to the client
await clientChain;
await socket.CloseJsonAsync(new { Message = "Success" });
//VoidResult is a special class that does nothing (preventing the header already sent exception)
return new VoidResult();
}
}
}
Content from Index.cshtml
<div>
<button class="slowtask-button">Start Long Task</button>
</div>
<div>
<progress class="slowtask-progress" value="0.0" max="1" />
</div>
<div>
<span class="slowtask-message">abc</span>
</div>
@section Scripts {
<script>
function websocketURL(url) {
var a = document.createElement('a');
a.href = url;
var url = a.cloneNode(false).href;
return url
.replace("https://", "wss://")
.replace("http://", "ws://");
}
$(function () {
$(".slowtask-button").on("click", function () {
var socket = new WebSocket(websocketURL("@Url.Action("SlowTask")"));
socket.onmessage = function (e) {
var message = JSON.parse(e.data)
$(".slowtask-message").text(message.Message);
$(".slowtask-progress").attr("value", message.ProgressValue);
$(".slowtask-progress").attr("max", message.ProgressMax);
};
socket.onclose = function (e) {
var message = JSON.parse(e.reason);
$(".slowtask-message").text(message.Message);
};
});
});
</script>
}