When building modern applications, there are often situations where you want to perform tasks in the background without holding up the main flow of your application. This is where “fire-and-forget” methods come into play. In C#, a fire-and-forget method allows you to run a task without awaiting its completion.
A common use case is sending a confirmation email after creating a user, but this also brings some limitations, particularly when dealing with scoped services. In this blog, we’ll walk through an example and explain why scoped services, like database contexts or HTTP clients, cannot be accessed in fire-and-forget methods.
Why Fire-and-Forget?
Fire-and-forget is useful in situations where you don’t need the result of an operation immediately, and it can happen in the background, for example:
- Sending emails
- Logging
- Notification sending
Here’s a common scenario where fire-and-forget comes in handy: sending a welcome email after a user is created in the system.
public async Task<ServiceResponse<User?>> AddUser(User user)
{
var response = new ServiceResponse<User?>();
try
{
// User creation logic (password hashing, saving to DB, etc.)
_context.Users.Add(user);
await _context.SaveChangesAsync();
// Fire-and-forget task to send an email
_ = Task.Run(() => SendUserCreationMail(user));
response.Data = user;
response.Message = user.FirstName + " added successfully";
}
catch (Exception ex)
{
// Error handling
}
return response;
}
The method SendUserCreationMail sends an email after a user is created, ensuring that the main user creation logic isn’t blocked by the email-sending process.
private async Task SendUserCreationMail(int id)
{
// This will throw an exception be _context is an scoped service
var user = await _context.Users.FindAsync(id);
var applicationUrl = "https://blogs.shahriyarali.com"
string body = $@"
<body>
<p>Dear {user.FirstName},</p>
<p>A new user has been created in the system:</p>
<p>Username: {user.Username}</p>
<p>Email: {user.Email}</p>
<p>Welcome to the system! Please use the provided username and email to log in. You can access the system by clicking on the following link:</p>
<p><a href='{applicationUrl}'>{applicationUrl}</a></p>
<p>Best regards,</p>
<p>Code With Shahri</p>
</body>";
var mailParameters = new MailParameters
{
Subject = $"New User Created - {user.Username}",
Body = body,
UserEmails = new List<UserEmail> { new() { Name = user.FirstName, Email = user.Email } }
};
await _mailSender.SendEmail(mailParameters);
}
In the code above, the SendUserCreationMail method is executed using Task.Run(). Since it's a fire-and-forget task, we don’t await it, allowing the user creation process to complete without waiting for the email to be sent.
The Problem with Scoped Services
A major pitfall with fire-and-forget tasks is that you cannot reliably access scoped services (such as DbContext or ILogger) within the task. This is because fire-and-forget tasks continue to run after the HTTP request has been completed, and by that point, scoped services will be disposed of.
For example, if _mailSender was scoped services, they could be disposed of before the SendUserCreationMail task completes, leading to exceptions.
Why Can’t We Access Scoped Services?
Scoped services have a lifecycle tied to the HTTP request in web applications. Once the request ends, these services are disposed of, meaning they are no longer available in any background task that wasn’t awaited within the request lifecycle.
In the example above, since the fire-and-forget email sending isn’t awaited, attempting to use scoped services will throw an ObjectDisposedException.
To safely access scoped services in a fire-and-forget method, you can leverage IServiceScopeFactory to manually create a service scope, ensuring that the services are available for the task.
private async Task SendUserCreationMail(int id)
{
// Create a service scope.
using var scope = _serviceScopeFactory.CreateScope();
var _context = scope.ServiceProvider.GetRequiredService<DataContext>();
var user = await _context.Users.FindAsync(id);
var applicationUrl = "https://blogs.shahriyarali.com"
string body = $@"
<body>
<p>Dear {user.FirstName},</p>
<p>A new user has been created in the system:</p>
<p>Username: {user.Username}</p>
<p>Email: {user.Email}</p>
<p>Welcome to the system! Please use the provided username and email to log in. You can access the system by clicking on the following link:</p>
<p><a href='{applicationUrl}'>{applicationUrl}</a></p>
<p>Best regards,</p>
<p>Code With Shahri</p>
</body>";
var mailParameters = new MailParameters
{
Subject = $"New User Created - {user.Username}",
Body = body,
UserEmails = new List<UserEmail> { new() { Name = user.FirstName, Email = user.Email } }
};
await _mailSender.SendEmail(mailParameters);
}
Conclusion
Fire-and-forget methods in C# are useful for executing background tasks without blocking the main application flow, but they come with their own set of challenges, particularly when working with scoped services. By leveraging techniques like IServiceScopeFactory, you can safely access scoped services in fire-and-forget tasks without risking lifecycle management issues. Whether you're sending emails, logging, or processing notifications, ensuring proper resource management is crucial to prevent errors like ObjectDisposedException. Always weigh the pros and cons of fire-and-forget and consider alternative approaches like background services or message queuing for more robust solutions in larger systems.
To explore more on this topic, you can check out the following resources on Microsoft Learn: