NOTE: Apart from
(and even then it's questionable, I'm Scottish). These are machine translated in languages I don't read. If they're terrible please contact me.
You can see how this translation was done in this article.
Thursday, 22 August 2024
//Less than a minute
This is a dumb little article because I was a bit confused about how to ensure that my IHostedService
was a single instance. I thought it was a bit more complicated than it actually was. So I thought I'd write a little article about it. Just in case anyone else was confused about it.
In the prior article, we covered how to create a background service using the IHostedService
interface for sending emails. This article will cover how to ensure that your IHostedService
is a single instance.
This might be obvious to some, but it's not to others (and wasn't immediately to me!).
Well its an issue as most of the articles out these cover how to use a IHostedService
but they don't cover how to ensure that the service is a single instance. This is important as you don't want multiple instances of the service running at the same time.
What do I mean? Well in ASP.NET the way to register an IHostedService or IHostedlifeCycleService (basically the same with more overrides for lifecycle management) you use this
services.AddHostedService(EmailSenderHostedService);
What that does is calls into this backend code:
public static IServiceCollection AddHostedService<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THostedService>(this IServiceCollection services)
where THostedService : class, IHostedService
{
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService, THostedService>());
return services;
}
Which is fine and dandy but what if you want to post a new message directly to this service from say a Controller
action?
public class ContactController(EmailSenderHostedService sender,ILogger<BaseController> logger) ...
{
[HttpPost]
[Route("submit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Submit([Bind(Prefix = "")] ContactViewModel comment)
{
ViewBag.Title = "Contact";
//Only allow HTMX requests
if(!Request.IsHtmx())
{
return RedirectToAction("Index", "Contact");
}
if (!ModelState.IsValid)
{
return PartialView("_ContactForm", comment);
}
var commentHtml = commentService.ProcessComment(comment.Comment);
var contactModel = new ContactEmailModel()
{
SenderEmail = string.IsNullOrEmpty(comment.Email) ? "Anonymous" : comment.Email,
SenderName = string.IsNullOrEmpty(comment.Name) ? "Anonymous" : comment.Name,
Comment = commentHtml,
};
await sender.SendEmailAsync(contactModel);
return PartialView("_Response",
new ContactViewModel() { Email = comment.Email, Name = comment.Name, Comment = commentHtml });
return RedirectToAction("Index", "Home");
}
}
Either you need to create an interface which itself implements IHostedService
then call into the method on that or you need to ensure that the service is a single instance. The latter is the easiest way to do this (depends on your scenario though, for testing the Interface method might be preferred).
You'll note here that it registers the service as an IHostedService
, this is to do with the lifecycle management of this service as the ASP.NET framework will use this registration to fire the events of this service (StartAsync
and StopAsync
for IHostedService). See below, IHostedlifeCycleService
is just a more detailed version of IHostedService.
/// <summary>
/// Defines methods for objects that are managed by the host.
/// </summary>
public interface IHostedService
{
/// <summary>
/// Triggered when the application host is ready to start the service.
/// </summary>
/// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
/// <returns>A <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous Start operation.</returns>
Task StartAsync(CancellationToken cancellationToken);
/// <summary>
/// Triggered when the application host is performing a graceful shutdown.
/// </summary>
/// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param>
/// <returns>A <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous Stop operation.</returns>
Task StopAsync(CancellationToken cancellationToken);
}
namespace Microsoft.Extensions.Hosting
{
/// <summary>
/// Defines methods that are run before or after
/// <see cref="M:Microsoft.Extensions.Hosting.IHostedService.StartAsync(System.Threading.CancellationToken)" /> and
/// <see cref="M:Microsoft.Extensions.Hosting.IHostedService.StopAsync(System.Threading.CancellationToken)" />.
/// </summary>
public interface IHostedLifecycleService : IHostedService
{
/// <summary>
/// Triggered before <see cref="M:Microsoft.Extensions.Hosting.IHostedService.StartAsync(System.Threading.CancellationToken)" />.
/// </summary>
/// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
/// <returns>A <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous operation.</returns>
Task StartingAsync(CancellationToken cancellationToken);
/// <summary>
/// Triggered after <see cref="M:Microsoft.Extensions.Hosting.IHostedService.StartAsync(System.Threading.CancellationToken)" />.
/// </summary>
/// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
/// <returns>A <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous operation.</returns>
Task StartedAsync(CancellationToken cancellationToken);
/// <summary>
/// Triggered before <see cref="M:Microsoft.Extensions.Hosting.IHostedService.StopAsync(System.Threading.CancellationToken)" />.
/// </summary>
/// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
/// <returns>A <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous operation.</returns>
Task StoppingAsync(CancellationToken cancellationToken);
/// <summary>
/// Triggered after <see cref="M:Microsoft.Extensions.Hosting.IHostedService.StopAsync(System.Threading.CancellationToken)" />.
/// </summary>
/// <param name="cancellationToken">Indicates that the stop process has been aborted.</param>
/// <returns>A <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous operation.</returns>
Task StoppedAsync(CancellationToken cancellationToken);
}
}
The Interface approach might be simpler depending on your scenario. Here you'd add an interface that inherits from IHostedService
and then add a method to that interface that you can call from your controller.
NOTE: You still need to add it as a HostedService in ASP.NET for the service to actually run.
public interface IEmailSenderHostedService : IHostedService, IDisposable
{
Task SendEmailAsync(BaseEmailModel message);
}
All we then need do is register this as a singleton and then use this in our controller.
services.AddSingleton<IEmailSenderHostedService, EmailSenderHostedService>();
services.AddHostedService<IEmailSenderHostedService>(provider => provider.GetRequiredService<IEmailSenderHostedService>());
ASP.NET will see that this has the correct interface decorated and will use this registration to run the IHostedService
.
Another to ensure that your IHostedService
is a single instance is to use the AddSingleton
method to register your service then pass the IHostedService
registration as a 'factory method'. This will ensure that only one instance of your service is created and used throughout the lifetime of the application.
services.AddSingleton<EmailSenderHostedService>();
services.AddHostedService(provider => provider.GetRequiredService<EmailSenderHostedService>());
So as you see here I first register my IHostedService
(or IHostedLifeCycleService
) as a singleton and then I use the AddHostedService
method to register the service as a factory method. This will ensure that only one instance of the service is created and used throughout the lifetime of the application.
As usual there's a couple of ways to skin a cat. The factory method approach is also a good way to ensure that your service is a single instance. It's up to you which approach you take. I hope this article has helped you understand how to ensure that your IHostedService
is a single instance.