The Missing Check Logs: An Asynchronous Logging Trap in a C# HIS Project

发布于:2025-08-10 ⋅ 阅读:(19) ⋅ 点赞:(0)

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:

  1. Synchronously update the patient’s status
  2. 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 and DbContext 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.


网站公告

今日签到

点亮在社区的每一天
去签到