A few weeks ago, something odd happened during a routine audit of our hospital information system (HIS). Some patients’ diagnostic procedures—CT scans, X-rays, blood tests—had been properly completed, but the system contained no logs to prove it.
No errors. No crash. Nothing.
The logs were just… gone.
The Setup
In our HIS system, every time a diagnostic check is completed, an API call is triggered:
POST /api/patient/CheckCompleted
This endpoint is supposed to do two things:
- Synchronously update the patient’s status
- Asynchronously log the event into the
CheckEventLog
table
Here’s the asynchronous method we originally used:
private void SaveCheckEventLogAsync(Patient patient, string checkType, DateTime checkTime, string doctor)
{
_ = Task.Run(async () =>
{
try
{
using (var logManager = new CheckEventManager(_logger, _connectionString))
{
await logManager.SaveCheckEventLogInternalAsync(
patient?.PatientID,
checkType,
checkTime,
doctor
);
}
}
catch (Exception ex)
{
_logger?.Error($"Async check log failed: {ex.Message}", ex);
}
});
}
We thought this was clean and non-blocking. We were wrong.
When Logs Go Missing
After reviewing several request-response pairs, we noticed something disturbing.
Here’s an actual log from a failed case:
Check completed: PatientID=200871, Type=CT Scan
Calling SaveCheckEventLogAsync
Starting Task.Run for SaveCheckEventLogAsync
SaveCheckEventLogAsync called
// ...nothing after this
And here’s a successful one using a synchronous fallback:
[Sync] SaveCheckEventLogSync called
[Sync] Created CheckEventLog entity
[Sync] Added to EF context
[Sync] SaveContextChanges completed
[Sync] Logging complete
We checked the CheckEventLog
table. The async record was never saved. There was no crash, no error, just silence.
The Root Cause
It took some digging, but the culprit was clear:
We were using
Task.Run
inside a controller, depending on services andDbContext
tied to the main request scope. Once the request ended, the dependencies were gone.
In C#, Task.Run()
executes on a background thread, but it doesn’t keep ASP.NET’s dependency injection context alive. So if your background task starts too late, it will try to use a disposed DbContext
, and quietly fail—or worse, do nothing at all.
The Fix: Pre-Build and Isolate
We changed our strategy entirely. Instead of letting the async method build its own dependencies mid-flight, we made the main thread do all the prep work.
Step 1: Build the log entry synchronously
var logEntry = new CheckEventLog
{
PatientID = patient.PatientID,
CheckType = checkType,
CheckTime = checkTime,
Doctor = doctor,
CreatedDate = DateTime.Now
};
This ensures we don’t touch any injected service in the background thread.
Step 2: Launch a completely isolated task
_ = Task.Run(async () =>
{
try
{
using (var manager = new CheckEventManager(_logger ?? Log.Logger, _connectionString))
{
manager.CheckEventLogs.Add(logEntry);
await manager.SaveChangesAsync();
}
}
catch (Exception ex)
{
_logger?.Error($"Async check log failed: {ex.Message}", ex);
}
});
We initialize a new instance of CheckEventManager
, which creates its own DbContext
using a raw connection string—not through DI. This ensures that the task has its own fully functional resource scope.
The Outcome
We deployed this fix and ran intensive validation:
- ✅ 1000+ parallel requests with no missing logs
- ✅ 30-minute stress test, full integrity
- ✅ No more complaints from auditing or QA teams
More importantly, the new logic is simpler and far easier to trace. Logs are either written or errors are captured.
The Takeaway
If you’re using C# with ASP.NET, remember:
- Never depend on DI-injected services inside
Task.Run()
- Never assume
Task.Run()
means “safe background execution” - Always isolate logging or background work into independent scopes
- Prepare your data before the task begins
This issue could’ve cost our client serious legal trouble—imagine a patient dispute where no system evidence of a scan exists. Fortunately, we caught it early.
If you’re building systems with async logging or background audit records—especially in critical environments like healthcare—make sure your background code can live without its parent.
Otherwise, one day, it might just quietly disappear.