Now that I have an API for my client to pull from I want to extend my chatter chat application to be able to hit it.
Portions of this post were inspired by IAmTimCorey’s video on Background Jobs in ASP.NET Core. Other parts derive from the Microsoft documentation on Sytem.Threading.Timer
I’m going to start by creating a new project, ChatterChatHandlerBackgroundJob, that will query the API, posting and getting messages from it. Rather than integrating this into the application, I’ve made each handler it’s own project with the idea that I want to use a plug-in architecture to allow for users to define their own chat handlers. In the solution provided this isn’t implemented yet but it is coming in future posts.
Yes, this is a contrived example and I don’t expect anyone would actually do that with this. However, it does allow us to walk through a lot of the technologies involved with spinning up a new application.
As discussed many posts ago, there is a abstract class in the solution called, “ChatHandler”. It is expected that all plug-in handlers will extend that class. In the abstract there is name, description, the ctor and SendChatAsync.
If we consider the use-case of a handler that will send messages to an API with a post and query for messages on a regular basis, we know we’ll need a timer to fire the get and an HttpClient to perform the get. Note that there are some great nuget packages out there for handling background jobs better than what I’ve shown here. But for the simplest case this will handle everything we need to get done.
I’ll also need to keep track of the last message I got by id so I know what to query for on the go-round.
Now, I need to consider what happens if I’m sending messages while I’m updating messages. It’s possible the two may overlap since the timer runs on it’s own thread separate from the call to send the message. It’s very possible that things will overlap and I may get my own message that I’ve just sent to the API back in my own chat. This would result in duplicate messages showing up in the chat window.
The easy solution with be to do a lock. Except I can’t await in a lock and I need to call my get/post methods from the HttpClient with an await. Why can’t I await? Well, a surprising amount of what we code into C# isn’t what the final code looks like. When doing the pre-compile, the code that gets sent to the compiler has the Get/Set changed out to real methods. The easiest way to view this is to download dotPeek from JetBrains. await is also swapped out for background threading functionality. await is syntactic sugar. It makes the code sweeter, that is, it is easier to read, use and understand. In the end, however, the code is wrapped in a background thread that you never see. That ends up causing issues with trying to lock around waiting for a call from the await.
The solution around this is to use a SemaphoreSlim. This is a lightweight way to tell the running thread to wait until it can enter.
In my BackgroundChatHandler I have:
static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1);
This relies on the semaphore to only allow one entry at a time. Then in my code I can call:
await semaphoreSlim.WaitAsync();
try
{
//do a bunch of stuff
}
finally
{
semaphoreSlim.Release();
}
The try/finally is very important so that it doesn’t cause everything to deadlock as the Release is what actually allows other threads to continue on.
Looking at my ctor, I have:
public BackgroundChatHandler(IMessenger messenger) : base(messenger)
{
client = new HttpClient();
timer = new Timer(GetRecentMessages, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
}
For the client, it’s generally recommended to use the same HttpClient over and over. For the timer, this is a System.Threading.Timer. .NET has what seems like sixteen million different timers but that is the one we want. The first parameter is the delegate to call when the timer hits it’s period. The second is the object state to pass in. Since we’re not using anything it’s null. The third parameter is how long after construction to start firing. Passing in TimeSpan.Zero just means start now. The fourth parameter is the period between firing the delegate. So I’m saying here, “Every second, call GetRecentMessages”.
In the class I do cheat with the base url. Looking in the launchSettings.json of our API, there is the url we’re telling our API to run at. In a production environment this would be configurable but for our purposes I can just hard-code it. Which leads us to the SendChatAsync.
public override async Task<bool> SendChatAsync(WriteToChatMessage message)
{
var url = $"{baseUrl}/sendMessage";
var jsonContent = JsonSerializer.Serialize(message);
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
//wait on any currently running post/get
await semaphoreSlim.WaitAsync();
try
{
var response = await client.PostAsync(url, content);
if (!response.IsSuccessStatusCode)
return false;
var jsonResponse = await response.Content.ReadAsStringAsync();
var messageFromServer = JsonSerializer.Deserialize<WriteToChatMessage>(jsonResponse);
if (messageFromServer != null)
{
Messenger.Send(messageFromServer);
lastMessageId = messageFromServer.messageId.HasValue ? messageFromServer.messageId.Value : 0;
}
return true;
}
finally
{
semaphoreSlim.Release();
}
}
- We’re sending over our WriteToChatMessage as json so serialize the message and create a content object for the HttpClient with the appropriate MimeType.
- Before we actually try to send it, see if we need to wait because the timer may be pulling down chat messages and wait if needed.
- Use the HttpClient to post the content to the url.
- Make sure it was successful.
- Get the response content.
- Deserialize it
- Send it to the Messenger
- Save the lastMessageId
- Release the semaphore
- and return.
Great! So we can now send messages to the API. Now lets get any messages we may have pending.
private async void GetRecentMessages(object? state)
{
List<WriteToChatMessage> messages = new List<WriteToChatMessage>();
//wait on any currently running post/get
await semaphoreSlim.WaitAsync();
try
{
//save the id since we're going to increment it in the query with skip/take
int lastId = lastMessageId;
int skip = 0; int take = 10;
for (; ; skip += take)
{
var url = $"{baseUrl}/getMessages/{lastId}?skip={skip}&take={take}";
var response = await client.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
Messenger.Send(new WriteToChatMessage("system", "unable to get messages from server", "Red", "White", "Normal"));
return;
}
var jsonResponse = await response.Content.ReadAsStringAsync();
var messagesFromServer = JsonSerializer.Deserialize<List<WriteToChatMessage>>(jsonResponse);
foreach (var message in messagesFromServer.OrderBy(x => x.messageId))
{
Messenger.Send(message);
if (message.messageId.HasValue)
lastMessageId = message.messageId.Value;
}
if (messagesFromServer.Count < take)
break;
}
}
finally
{
semaphoreSlim.Release();
}
}
- Wait on the semaphore until we can try to get messages
- loop through with skip/take since we know we can only get 10 messages at a time
- build the url as defined in the API
- Call get on the HttpClient
- If it wasn’t a success, send a message on the Messenger to tell the user and return
- Read the response and deserialize
- foreach message we got, send via the Messenger
- save the last id so we know where to query from the next time we’re called
- if the number of messages is less then what we wanted to take, then break from the loop.
- release the semaphore
And there it is. Next I’ll add in SignalR to our Web API and write a chat handler for that.
Thanks,
Brian
One thought on “Adding a background job with a timer to pull from ASP.NET Core Web API”