3
\$\begingroup\$

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:

  1. Is there a better or more concise way to do this?
  2. Is there anything I am overlooking with the code that may cause issues?
  3. 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>
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.