Over the last few months I've been working on an app called [OnxGraph](https://onxgraph.stevanfreeborn.com) which is a tool for administrators of [Onspring](https://onspring.com) to visualize relationships between their content. When I began building it I knew that I was going to have to rely on talking to Onspring's public API to get the data I needed to display the graph's nodes and edges. However there is no way for me to know ahead of time how much data I would be dealing with. I could be dealing with a few nodes and edges which would only require a handful of API requests or many more that would require many API requests.
This presented the challenge of how to make sure that a user's request to create a graph didn't timeout while waiting for the data to be fetched as well as provide feedback to the user about the progress of the request. I decided this best approach was not to do all this work inline with the request but instead to queue the work and then provide the user with a way to check on the progress of the request once it was dequeued and processing.
The easiest solution here was probably just do some long polling. However I've been wanting to get some experience with [SignalR](https://dotnet.microsoft.com/apps/aspnet/signalr) for a while now and this seemed like a good opportunity to do so. SignalR is a library that makes it easy to add real-time web functionality to your applications. It's built on top of [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) and abstracts away the complexity of managing connections. Plus as a fallback it will use polling if WebSockets aren't available.
I thought since I went through the process of setting this up for OnxGraph I'd write a short blog post about how to get it working. I'll be using a simple example of a task queue that processes tasks and sends updates to clients as the tasks are processed. We will have two parts to this example.
1. A simple [Vue.js](https://vuejs.org) client that will have a form to add a task to the queue and display the tasks added and update them after being processed.
2. A simple [ASP.NET Core](https://dotnet.microsoft.com/apps/aspnet) web api that will have a singleton service that manages an in-memory queue, a hosted background service that processes the queue and sends updates to the clients, an endpoint to add a task to the queue, and a hub that the clients can connect to to receive updates.
Update `launchSettings.json` so it runs on https by default. You just have to make sure the `https` profile is the first one in the profiles object so it is the default profile.
Using a debugger is great and I think everyone should be using one. If you want to debug either the client or the server you most definitely can in this case. I've set this up in the example repo using visual studio code. Take a look at the `.vscode/launch.json` file.
Now let's start with the client and add a button that when clicked will add a task to the queue. We will also display the status of the task. We will use the [Vue Composition API](https://v3.vuejs.org/guide/composition-api-introduction.html) to manage the state of the task.
Let's start by cleaning up the boiler plate code that comes with the web api template by updating `Program.cs` to remove all references to weather forecast.
Next we will change the `weatherforecast` endpoint to `add-task` endpoint. And use the `MapPost` method instead of `MapGet` to add the endpoint. We will start by just responding with a new task id.
### Make sure the client can add tasks to the queue
At this point we should be able to go to the client and click the button to add a task to the queue. You should see the status change to `Adding task...` and then `Task added!`. If you see `Failed to add task` then something went wrong. You can check the console for more information.
So that is cool. We can take a task from the client, send it to the server, and get a response back. But we need to actually do something with that task so it can actually be processed. Let's start by creating a class to represent the task.
We will use something really generic like a `BackgroundTask` class that has an `Id` property that is set to a new guid when the task is created.
Now that we can represent these tasks we need to create a queue to persist them while they are waiting to be processed. This in a real world scenario would likely be sorted by some sort of persistent store like RabbitMQ or Azure Service Bus. But for this example we will just use an in-memory queue implemented with a [Channels](https://learn.microsoft.com/en-us/dotnet/core/extensions/channels) and a class called `BackgroundTaskQueue` that we will register as a singleton service.
This class has two methods `EnqueueAsync` and `DequeueAsync`. The `EnqueueAsync` method will add a task to the queue and the `DequeueAsync` method will remove a task from the queue. We will be able to consume these methods in the `TaskService` class that we will create next.
We are getting close to having everything wired up. But we are still missing a way to actual process these tasks which we are receiving from the client and sticking in our queue. For this we can create a class called `TaskService` that will be a hosted service that will run in the background and continuously pull tasks out of the queue and process them.
In the service of keeping things super simple in the example we are just going to log the start and stop of the task and simulate some async work with a random delay between 5 and 30 seconds. In a real world scenario you would be doing some actual work here.
> [!NOTE]
> I am wrapping the processing work in a call to `Task.Run` because in this scenario we are firing and forgetting the task and I don't want to block the background service from processing other tasks. In a real world scenario you would want to be more careful about how you handle exceptions when doing this.
Great we got our queue and we've got a service to process that queue, but our tasks aren't actually yet going into the queue even thought they are making it to the server. Let's update the `add-task` endpoint to actually add the task to the queue.
However we still haven't done anything to address the initial problem of providing feedback to the client about the progress of the task as it is being processed. Let's do that now.