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\$

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.