Scenario
Suppose you are developing a web application in which you have a lengthy process with the probability of many errors during the process. How can you provide a method to log the errors in the database without causing performance issues?
Here’s a basic method you can call in your catch blocks:
public async void LogAnError(string? theErrorMessage)
{
var currentDateTime = PersianDateTime.Now.ToString();
GeneratedErrors theError = new GeneratedErrors();
theError.MessageText = theErrorMessage;
theError.CreateDateTime = currentDateTime;
await _context.GeneratedErrors.AddAsync(theError);
await _context.SaveChangesAsync();
}
C#Meditations
Using await
in this method can be very useful for ensuring that the asynchronous operations (in this case, adding an error to the database and saving changes) complete before moving on. However, if this method is called in the middle of a lengthy process and you’re concerned about performance, you’ll need to consider a few things:
Execution Context
Using await
in an async void method can cause exceptions to be thrown outside the calling context, making them harder to handle.
It’s generally recommended to use async Task
for asynchronous methods to better handle exceptions and provide a clear path for await
.
Fire-and-Forget Pattern
If logging the error is not critical to the immediate success of the ongoing process and you can afford to log it in the background, you might consider making the call “fire-and-forget.”
In this case, you can use Task.Run
to log the error asynchronously.
So, to improve the current method, you might want to return a Task
instead of void, which makes error handling more manageable. Here’s an updated version:
public async Task LogAnError(string? theErrorMessage)
{
var currentDateTime = PersianDateTime.Now.ToString();
GeneratedErrors theError = new GeneratedErrors();
theError.MessageText = theErrorMessage;
theError.CreateDateTime = currentDateTime;
await _context.GeneratedErrors.AddAsync(theError);
await _context.SaveChangesAsync();
}
C#Fire-and-Forget
Now, let’s consider another approach we just talked about, fire-and-forget:
public async Task LogAnError(string? theErrorMessage)
{
Task.Run(async () =>
{
var currentDateTime = PersianDateTime.Now.ToString();
GeneratedErrors theError = new GeneratedErrors();
theError.MessageText = theErrorMessage;
theError.CreateDateTime = currentDateTime;
await _context.GeneratedErrors.AddAsync(theError);
await _context.SaveChangesAsync();
});
}
C#More Meditations and Ruminations
async void
: Suitable for event handlers and truly fire-and-forget operations, but harder to manage errors.async Task
: Preferred for asynchronous methods to properly handle exceptions and await completion.
Use Task.Run
if logging the error can be done in the background without disrupting the main process.
But what about multiple simultaneous calls of the method? Can the fire-and-forget approach manage them? the short answer is damn yes. The longer answer reads like this: A fire-and-forget method can handle multiple simultaneous calls. Since each invocation of the method starts a new task that runs independently, multiple errors can be recorded simultaneously. However, there are some considerations to ensure the method performs well under high concurrency:
Concurrency Handling
Thread Safety: Ensure that the database context (_context
) is thread-safe. Using dependency injection with scoped services typically ensures each request gets its own instance of the context.
Database Performance: The database should be capable of handling multiple concurrent writes. This includes having proper indexing and optimized database configurations.
Exception Handling
Robust Error Handling: Each task should handle exceptions internally to avoid unhandled exceptions that can crash the application. Logging or retry mechanisms can be implemented within the task.
Resource Management
Connection Pooling: Ensure the database connection pool is configured to handle the expected load. This prevents exhaustion of connections during high concurrency.
Task Management: Be mindful of the number of tasks being created. Excessive tasks can lead to resource exhaustion and degrade performance.
Implementation
Here’s how you can implement the fire-and-forget method with proper exception handling:
public async Task LogAnError(string? theErrorMessage)
{
Task.Run(async () =>
{
try
{
var currentDateTime = PersianDateTime.Now.ToString();
GeneratedErrors theError = new GeneratedErrors();
theError.MessageText = theErrorMessage;
theError.CreateDateTime = currentDateTime;
await _context.GeneratedErrors.AddAsync(theError);
await _context.SaveChangesAsync();
}
catch (Exception ex)
{
// Log the exception or handle it as needed
Console.WriteLine($"An error occurred while logging: {ex.Message}");
}
});
}
C#Summing Up
Thread Safety: Ensure the database context is scoped correctly.
Concurrency: The database must be capable of handling concurrent writes.
Exception Handling: Robustly handle exceptions within each task.
Resource Management: Configure connection pooling and manage task creation to avoid resource exhaustion.