ConfigureAwait Best Practices
Overview
This document outlines the proper usage of ConfigureAwait() in the arolariu.ro codebase, explaining when to use it, when to omit it, and the rationale behind these decisions.
Quick Reference
| Context | Rule | Rationale |
|---|---|---|
| Production Code | ✅ Always use ConfigureAwait(false) | Prevents deadlocks, improves performance |
| Test Methods | ❌ Never use ConfigureAwait() | Bypasses test parallelization, unnecessary overhead |
| ASP.NET Controllers | ✅ Use ConfigureAwait(false) | No synchronization context after first await |
| Library Code | ✅ Use ConfigureAwait(false) | Should not assume execution context |
Production Code Guidelines
✅ DO: Use ConfigureAwait(false) in Library Code
Why? Library code should not assume it will run on a specific synchronization context.
// ✅ CORRECT
public async Task<Invoice> GetInvoiceAsync(Guid id)
{
var data = await _repository.FetchAsync(id).ConfigureAwait(false);
var result = await ProcessDataAsync(data).ConfigureAwait(false);
return result;
}
// ❌ INCORRECT - Missing ConfigureAwait
public async Task<Invoice> GetInvoiceAsync(Guid id)
{
var data = await _repository.FetchAsync(id); // CA2007 warning
var result = await ProcessDataAsync(data); // CA2007 warning
return result;
}
✅ DO: Use ConfigureAwait(false) in ASP.NET Core
Why? After the first await, ASP.NET Core doesn't restore the synchronization context, so you should explicitly use ConfigureAwait(false) for clarity and performance.
// ✅ CORRECT
[HttpGet("{id}")]
public async Task<ActionResult<Invoice>> GetInvoice(Guid id)
{
var invoice = await _service.GetInvoiceAsync(id).ConfigureAwait(false);
return Ok(invoice);
}
✅ DO: Chain ConfigureAwait(false) for All Awaits
Why? Consistency prevents accidental context captures and makes code behavior predictable.
// ✅ CORRECT - All awaits use ConfigureAwait(false)
public async Task<ProcessedData> ProcessAsync(InputData data)
{
var step1 = await ValidateAsync(data).ConfigureAwait(false);
var step2 = await TransformAsync(step1).ConfigureAwait(false);
var step3 = await EnrichAsync(step2).ConfigureAwait(false);
return await FinalizeAsync(step3).ConfigureAwait(false);
}
Test Code Guidelines
❌ DON'T: Use ConfigureAwait in Test Methods
Why? Test frameworks manage their own synchronization contexts for proper parallelization.
// ✅ CORRECT - No ConfigureAwait in tests
[Fact]
public async Task GetInvoice_ValidId_ReturnsInvoice()
{
// Arrange
var expectedInvoice = InvoiceBuilder.CreateRandomInvoice();
_mockRepository.Setup(r => r.GetAsync(It.IsAny<Guid>()))
.ReturnsAsync(expectedInvoice);
// Act
var result = await _service.GetInvoiceAsync(Guid.NewGuid());
// Assert
Assert.NotNull(result);
Assert.Equal(expectedInvoice.id, result.id);
}
// ❌ INCORRECT - ConfigureAwait in test bypasses parallelization
[Fact]
public async Task GetInvoice_ValidId_ReturnsInvoice()
{
var result = await _service.GetInvoiceAsync(Guid.NewGuid()).ConfigureAwait(false);
Assert.NotNull(result);
}
Build Configuration for Tests
Test projects automatically disable CA2007 warnings via Directory.Build.props:
<!-- sites/api.arolariu.ro/tests/Directory.Build.props -->
<PropertyGroup Label="Test Project Settings">
<NoWarn>$(NoWarn);CA2007</NoWarn>
</PropertyGroup>
This means:
- ✅ Test code doesn't need
ConfigureAwaitcalls - ✅ No CA2007 warnings in test files
- ✅ Simpler, cleaner test code
- ✅ Proper test parallelization behavior
Common Scenarios
Scenario 1: Domain Service Methods
// ✅ CORRECT
public async Task<Invoice> AnalyzeInvoiceAsync(Invoice invoice)
{
// All library/service code uses ConfigureAwait(false)
var validated = await ValidateDomainRulesAsync(invoice).ConfigureAwait(false);
var enriched = await EnrichWithAIAsync(validated).ConfigureAwait(false);
return await ApplyBusinessLogicAsync(enriched).ConfigureAwait(false);
}
Scenario 2: Repository/Data Access
// ✅ CORRECT
public async Task<Invoice> GetByIdAsync(Guid id, Guid? userId)
{
var container = await GetContainerAsync().ConfigureAwait(false);
var response = await container.ReadItemAsync<Invoice>(
id.ToString(),
new PartitionKey(userId?.ToString() ?? string.Empty)
).ConfigureAwait(false);
return response.Resource;
}
Scenario 3: Exception Handling
// ✅ CORRECT - ConfigureAwait in try-catch blocks
private async Task<Invoice> TryCatchAsync(ReturningInvoiceFunction function)
{
try
{
return await function().ConfigureAwait(false);
}
catch (ValidationException ex)
{
throw CreateAndLogValidationException(ex);
}
catch (DependencyException ex)
{
throw CreateAndLogDependencyException(ex);
}
}
Scenario 4: Integration Tests (Different Rules!)
// ✅ CORRECT - Integration tests can omit ConfigureAwait
[Fact]
public async Task EndToEnd_CreateAndRetrieveInvoice_Success()
{
// Arrange
var client = _factory.CreateClient();
var invoice = CreateTestInvoice();
// Act
var createResponse = await client.PostAsJsonAsync("/api/invoices", invoice);
var getResponse = await client.GetAsync($"/api/invoices/{invoice.id}");
// Assert
createResponse.EnsureSuccessStatusCode();
getResponse.EnsureSuccessStatusCode();
}
Performance Implications
With ConfigureAwait(false)
- ✅ Avoids unnecessary context switches
- ✅ Better thread pool utilization
- ✅ Prevents potential deadlocks in library code
- ✅ Lower memory overhead (no context capture)
Without ConfigureAwait (in production)
- ❌ Potential deadlocks in synchronous-over-async code
- ❌ Unnecessary context captures
- ❌ Reduced throughput due to context switches
- ❌ Higher memory allocation
Without ConfigureAwait (in tests)
- ✅ Correct behavior - test frameworks need context control
- ✅ Proper test parallelization
- ✅ Simpler, more readable test code
Analyzer Rules
CA2007: Do not directly await a Task
Severity: Warning (Production), Silent (Tests)
Configuration:
- Production projects:
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> - Test projects:
<NoWarn>$(NoWarn);CA2007</NoWarn>
EditorConfig:
# Production code - enforce ConfigureAwait
[**/src/**/*.cs]
dotnet_diagnostic.CA2007.severity = warning
# Test code - disable ConfigureAwait requirement
[**/tests/**/*.cs]
dotnet_diagnostic.CA2007.severity = none
Migration Guide
Fixing Existing Code
Step 1: Run analyzer to find violations
dotnet build /p:EnforceCodeStyleInBuild=true
Step 2: Fix production code violations
// Before
await SomeMethodAsync();
// After
await SomeMethodAsync().ConfigureAwait(false);
Step 3: Remove ConfigureAwait from test code
// Before
await orchestrationService.CreateInvoiceObject(invoice, null).ConfigureAwait(false);
// After
await orchestrationService.CreateInvoiceObject(invoice, null);
References
- ConfigureAwait FAQ
- CA2007: Do not directly await a Task
- Async/Await Best Practices
- Test Parallelization in xUnit
Summary
| Context | ConfigureAwait | Reason |
|---|---|---|
| Service layer | ✅ .ConfigureAwait(false) | Library code |
| Repository layer | ✅ .ConfigureAwait(false) | Data access code |
| Controllers | ✅ .ConfigureAwait(false) | ASP.NET Core best practice |
| Domain logic | ✅ .ConfigureAwait(false) | Pure business logic |
| Unit tests | ❌ Omit | Test framework control |
| Integration tests | ❌ Omit | Test framework control |
Golden Rule:
- Production = Always ConfigureAwait(false)
- Tests = Never ConfigureAwait