This article outlines how to invoke a long running operation at a REST end-point. The core idea revolves around assuring the REST end-point returns immediately, while the responsibility of communicating the result of long runnning process back to the client is transferred to a web-socket.
REST Endpoint.
The first task involves creating a REST Endpoint which would trigger the long runnig process.
[HttpPost]
[Route("StartTask")]
public ActionResult StartTask()
{
Task.Run(()=> LongRunningTask());
return Accepted();
}
The controller action is pretty straight-forward. It is a POST method, which triggers the long-running process. But it does not wait for the result, instead it does a fire-and-forget. It returns a HTTP 202 status as the result of the REST end-point call.
One can argue that we should run the long running process in a IHostedService or using tools like Hangfire (or atleast with property exception handling), but for the sake of this example, we will keep it simple with a Task.Run().
Long Runnig Task.
The long running task would do some long running process (which is not interesting). The real magic happens after the process is completed and we have the result to send to the client. This is where we use web-socket.
private async Task LongRunningTask()
{
for(int i = 0; i < 100; i++)
{
await Task.Delay(1000);
}
await webSocketManager.SendResponse(new ApplicationMessageType(new ApplicationData("Job Completed","Information")));
}
The webSocketManager.SendResponse() method sends the back the response to the client. We will shortly look into the implementation of the same, but for now, remember this step.
Web Socket EndPoint.
In this step, we will create a WebSocketController which would expose the web-socket from the server.
[ApiController]
[Route("[controller]")]
public class WebSocketController(IWebSocketManager webSocketManager): ControllerBase
{
[HttpGet]
[Route("ws")]
public async Task<ActionResult> HandleSocket()
{
if (HttpContext.WebSockets.IsWebSocketRequest)
{
// Accept the WebSocket request
WebSocket webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
webSocketManager.AddSocket(webSocket);
await HandleWebSocketConnection(webSocket);
return Ok();
}
else
{
return BadRequest("WebSocket connection expected.");
}
}
}
The end-point accepts a incoming web-socket connection from the client. We first check if the incoming request is a Web Socket Request and if so, accept the connection. We also add the web socket details to our internal cache (we will inspect in a while). We then will invoke the HandleWebSocketConnection() which is responsible for handling incoming/outgoing messages.
private async Task HandleWebSocketConnection(WebSocket webSocket)
{
var buffer = new byte[1024 * 4]; // Buffer for receiving data
WebSocketReceiveResult result;
do
{
// Receive data from the WebSocket
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Text)
{
// Convert the received data to string (text message)
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
Console.WriteLine("Received: " + message);
}
// Check if the client sent a close request
if (result.MessageType == WebSocketMessageType.Close)
{
await webSocketManager.CloseConnection(webSocket);
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Connection closed by client", CancellationToken.None);
}
} while (!result.CloseStatus.HasValue); // Continue until the client requests to close the connection
// Close the WebSocket connection from the server's side when done
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
}
We check if the incoming message is a text message or a request to close the connection. If the later, we remove the connection details from our Socket Manager and close the particular connection from the server end.
The webSocketManager implementation is as follows.
public class WebSocketManager: IWebSocketManager
{
private List<WebSocket> _sockets = new List<WebSocket>();
public void AddSocket(WebSocket socket)
{
_sockets.Add(socket);
}
public async Task SendResponse<T>(T message)
{
foreach(var socket in _sockets)
{
if(socket.State == WebSocketState.Open)
{
var jsonMessage = JsonSerializer.Serialize(message);
var messageBytes = Encoding.UTF8.GetBytes(jsonMessage);
await socket.SendAsync(messageBytes, WebSocketMessageType.Text, true, CancellationToken.None);
}
}
}
public async Task CloseConnection(WebSocket socket)
{
_sockets.Remove(socket);
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing Connection", CancellationToken.None);
}
}
The WebSocketManager exposes method to Send response back to the Socket. We also maintain a collection of open socket connection, and send (for sake of this example) the results to each of the currently open socket connections.
We have one final step in the Server to do, which is to register the WebSocket middleware. This is done as follows
app.UseWebSockets();
Client
We could test the created connections using Postman, but for sake of completion let us go ahead and create a Vue 3 client for our testing our long running endpoint. We can trigger our end point as follows
async function Start() {
const response = await axios.post('https://localhost:7259/demo/StartTask');
}
As seen in the example code above, we are using Axios library to post a request to our StartTask endpoint. This would trigger our long running process. The next step involving setting up our Web Socket connection and listening to any feedbacks from the Server.
const socket = ref<WebSocket | null>(null);
onMounted(() => {
socket.value = new WebSocket(`wss://localhost:7259/WebSocket/ws`);
socket.value.onopen = () => {
console.log('WebSocket connection established.');
};
socket.value.onmessage = (event) => {
console.log('Message from server:', event.data);
var message = JSON.parse(event.data);
console.log('Deserialized Message from server:', message);
};
socket.value.onclose = () => {
console.log('WebSocket connection closed.');
};
})
As seen in the code above, we are creating a Web Socket connection using WebSocket. We have also added handlers for handling messages from the Server and handlers are handling OnOpen and OnClose events for the socket.
That is all we have to do. The complete code discussed here is available in my Github