Skip to main content

Anti-Pattern: Sync-over-Async (.Result / .Wait)

🔴 The Problem

Forcing an asynchronous operation to run synchronously by blocking the calling thread until it completes. This is a primary cause of Deadlocks in ASP.NET (legacy) applications.

The Mechanism

In ASP.NET (non-Core), the SynchronizationContext allows only one thread at a time to handle a request context.

  1. AsyncMethod() starts and captures the current context.
  2. .Result blocks the main thread, waiting for the async method.
  3. AsyncMethod() finishes and tries to re-enter the main context to complete.
  4. DEADLOCK: The context is blocked by .Result (main thread is waiting), so the async method can't finish re-entering. The main thread waits forever for the async method, which is waiting for the main thread.

🚱 Real WEP Example

Identified in WepGoogleApiServiceFactory.cs:

// ❌ DEADLOCK RISK
// Blocking the thread to wait for an async network call
bool success = credential.RefreshTokenAsync(CancellationToken.None).Result;

💡 The Solution

Go "Async All the Way". If you call an async method, your method must be async and use await.

// ✅ NON-BLOCKING
// Releases thread to pool while waiting for Google
bool success = await credential.RefreshTokenAsync(CancellationToken.None);

Alternatives (Pragmatic Last Resort)

If you are in a legacy synchronous path that absolutely cannot be refactored to async (e.g., constructors, legacy event handlers), see the Safe Harbor Policy.

// ⚠️ Pragmatic Last Resort: Safe-ish, but still blocking
var result = Task.Run(() => credential.RefreshTokenAsync(...)).Result;

Forensic Detection

Use these commands to find risky sync-over-async blocks:

# Find usage of .Result or .Wait()
grep -r "\.Result" . --include="*.cs"
grep -r "\.Wait(" . --include="*.cs"