Skip to content

Commit f269c3f

Browse files
committed
chore: enforce ConfigureAwait(false) across shipped code
1 parent e2206da commit f269c3f

62 files changed

Lines changed: 390 additions & 358 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/copilot-instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- When modifying a public member, make sure to check the Public.API files for the affected assembly and update them if necessary.
1212
- Do not ignore CS1591 warnings; analyze and add missing XML comments instead.
1313
- Comments inside method bodies must explain *why*, not *what*: a why-comment states a constraint, invariant, or rationale the code cannot express on its own (e.g. concurrency/ordering requirements, the reason for a workaround, a justification for otherwise-surprising deliberate behavior). Narration/what-comments — restating what the next line does, section-header comments, play-by-play commentary — are not allowed. For complex logic, prefer extracting it into a separate method with a descriptive name over adding a comment.
14+
- Library code under `src/` must call `ConfigureAwait(false)` on every `await` (including the implicit awaits in `await using`/`await foreach`) — `CA2007` is enforced as an error for `src/` only. Tests and samples are exempt and should not add it.
1415

1516
## Testing Guidelines
1617
- Prefer using the Vulthil.xUnit testing framework for tests.

src/.editorconfig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Policy: everything under src/ ships to NuGet as a library, so it must never capture the
2+
# caller's synchronization context. CA2007 is an error here (overriding the repo-wide default
3+
# in the root .editorconfig) so every await in shipped code calls ConfigureAwait(false).
4+
# tests/ and samples/ are consumers, not libraries, and stay exempt.
5+
[*.cs]
6+
dotnet_diagnostic.CA2007.severity = error

src/Vulthil.Extensions.Testing/HttpResponseMessageExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public static async Task<TResponse> GetResponseAsync<TResponse>(this HttpRespons
2121
ArgumentNullException.ThrowIfNull(response);
2222
response.EnsureSuccessStatusCode();
2323

24-
var result = await response.Content.ReadFromJsonAsync<TResponse>(cancellationToken) ?? throw new InvalidOperationException("Response content is empty or could not be deserialized.");
24+
var result = await response.Content.ReadFromJsonAsync<TResponse>(cancellationToken).ConfigureAwait(false) ?? throw new InvalidOperationException("Response content is empty or could not be deserialized.");
2525

2626
return result;
2727
}

src/Vulthil.Extensions.Testing/Polling.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public static async Task<PollingResult<T>> WaitAsync<T>(
120120
{
121121
do
122122
{
123-
Result<T> result = await func(linkedCts.Token);
123+
Result<T> result = await func(linkedCts.Token).ConfigureAwait(false);
124124

125125
if (result.IsSuccess)
126126
{
@@ -129,7 +129,7 @@ public static async Task<PollingResult<T>> WaitAsync<T>(
129129

130130
errors.Add(result.Error);
131131
}
132-
while (await timer.WaitForNextTickAsync(linkedCts.Token));
132+
while (await timer.WaitForNextTickAsync(linkedCts.Token).ConfigureAwait(false));
133133
}
134134
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
135135
{
@@ -234,7 +234,7 @@ public static async Task<PollingResult> WaitAsync(
234234
{
235235
do
236236
{
237-
Result result = await func(linkedCts.Token);
237+
Result result = await func(linkedCts.Token).ConfigureAwait(false);
238238

239239
if (result.IsSuccess)
240240
{
@@ -243,7 +243,7 @@ public static async Task<PollingResult> WaitAsync(
243243

244244
errors.Add(result.Error);
245245
}
246-
while (await timer.WaitForNextTickAsync(linkedCts.Token));
246+
while (await timer.WaitForNextTickAsync(linkedCts.Token).ConfigureAwait(false));
247247
}
248248
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
249249
{

src/Vulthil.Messaging.Inbox.Cosmos/CosmosIdempotencyStore.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,15 @@ public async Task<int> DeleteProcessedAsync(DateTimeOffset olderThanUtc, int bat
2626
var markers = await dbContext.InboxMessages
2727
.Where(marker => marker.ProcessedOnUtc < olderThanUtc)
2828
.Take(batchSize)
29-
.ToListAsync(cancellationToken);
29+
.ToListAsync(cancellationToken).ConfigureAwait(false);
3030

3131
if (markers.Count == 0)
3232
{
3333
return 0;
3434
}
3535

3636
dbContext.InboxMessages.RemoveRange(markers);
37-
await SaveRemovedMarkersAsync(cancellationToken);
37+
await SaveRemovedMarkersAsync(cancellationToken).ConfigureAwait(false);
3838
return markers.Count;
3939
}
4040

@@ -46,7 +46,7 @@ private async Task SaveRemovedMarkersAsync(CancellationToken cancellationToken)
4646
{
4747
try
4848
{
49-
await dbContext.SaveChangesAsync(cancellationToken);
49+
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
5050
}
5151
catch (DbUpdateConcurrencyException exception)
5252
{
@@ -67,12 +67,12 @@ public async Task<bool> ProcessAsync(string idempotencyKey, IMessageContext cont
6767
ArgumentException.ThrowIfNullOrEmpty(idempotencyKey);
6868
ArgumentNullException.ThrowIfNull(process);
6969

70-
if (await dbContext.InboxMessages.FindAsync([idempotencyKey], cancellationToken) is not null)
70+
if (await dbContext.InboxMessages.FindAsync([idempotencyKey], cancellationToken).ConfigureAwait(false) is not null)
7171
{
7272
return false;
7373
}
7474

75-
await process(cancellationToken);
75+
await process(cancellationToken).ConfigureAwait(false);
7676

7777
dbContext.InboxMessages.Add(new InboxMessage
7878
{
@@ -82,7 +82,7 @@ public async Task<bool> ProcessAsync(string idempotencyKey, IMessageContext cont
8282

8383
try
8484
{
85-
await dbContext.SaveChangesAsync(cancellationToken);
85+
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
8686
return true;
8787
}
8888
catch (DbUpdateException exception) when (IsConflict(exception))

src/Vulthil.Messaging.Inbox.Relational/RelationalIdempotencyStore.cs

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public async Task<int> DeleteProcessedAsync(DateTimeOffset olderThanUtc, int bat
3333
.OrderBy(marker => marker.ProcessedOnUtc)
3434
.Take(batchSize)
3535
.Select(marker => marker.MessageId)
36-
.ToListAsync(cancellationToken);
36+
.ToListAsync(cancellationToken).ConfigureAwait(false);
3737

3838
if (keys.Count == 0)
3939
{
@@ -42,7 +42,7 @@ public async Task<int> DeleteProcessedAsync(DateTimeOffset olderThanUtc, int bat
4242

4343
return await dbContext.InboxMessages
4444
.Where(marker => keys.Contains(marker.MessageId))
45-
.ExecuteDeleteAsync(cancellationToken);
45+
.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
4646
}
4747

4848
public Task<bool> ProcessAsync(string idempotencyKey, IMessageContext context, Func<CancellationToken, Task> process, CancellationToken cancellationToken)
@@ -57,34 +57,35 @@ public Task<bool> ProcessAsync(string idempotencyKey, IMessageContext context, F
5757

5858
var strategy = dbContext.Database.CreateExecutionStrategy();
5959
return strategy.ExecuteAsync(
60-
async token => await ProcessInOwnTransactionAsync(idempotencyKey, process, token),
60+
async token => await ProcessInOwnTransactionAsync(idempotencyKey, process, token).ConfigureAwait(false),
6161
cancellationToken);
6262
}
6363

6464
private async Task<bool> ProcessInOwnTransactionAsync(string idempotencyKey, Func<CancellationToken, Task> process, CancellationToken cancellationToken)
6565
{
6666
dbContext.ChangeTracker.Clear();
67-
await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
67+
var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
68+
await using var _ = transaction.ConfigureAwait(false);
6869

69-
if (await HasProcessedAsync(idempotencyKey, cancellationToken))
70+
if (await HasProcessedAsync(idempotencyKey, cancellationToken).ConfigureAwait(false))
7071
{
7172
return false;
7273
}
7374

74-
await process(cancellationToken);
75+
await process(cancellationToken).ConfigureAwait(false);
7576
AddMarker(idempotencyKey);
7677

7778
try
7879
{
79-
await dbContext.SaveChangesAsync(cancellationToken);
80-
await transaction.CommitAsync(cancellationToken);
80+
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
81+
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
8182
return true;
8283
}
8384
catch (DbUpdateException)
8485
{
85-
await transaction.RollbackAsync(cancellationToken);
86+
await transaction.RollbackAsync(cancellationToken).ConfigureAwait(false);
8687

87-
if (await HasProcessedAsync(idempotencyKey, cancellationToken))
88+
if (await HasProcessedAsync(idempotencyKey, cancellationToken).ConfigureAwait(false))
8889
{
8990
return false;
9091
}
@@ -95,14 +96,14 @@ private async Task<bool> ProcessInOwnTransactionAsync(string idempotencyKey, Fun
9596

9697
private async Task<bool> ProcessInAmbientTransactionAsync(string idempotencyKey, Func<CancellationToken, Task> process, CancellationToken cancellationToken)
9798
{
98-
if (await HasProcessedAsync(idempotencyKey, cancellationToken))
99+
if (await HasProcessedAsync(idempotencyKey, cancellationToken).ConfigureAwait(false))
99100
{
100101
return false;
101102
}
102103

103-
await process(cancellationToken);
104+
await process(cancellationToken).ConfigureAwait(false);
104105
AddMarker(idempotencyKey);
105-
await dbContext.SaveChangesAsync(cancellationToken);
106+
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
106107
return true;
107108
}
108109

src/Vulthil.Messaging.Inbox/IdempotentConsumeFilter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ public async Task ConsumeAsync(IMessageContext<TMessage> context, ConsumeDelegat
4545

4646
InboxLog.MissingKeyAllowed(_logger, _messageType);
4747
InboxTelemetry.MissingKey.Add(1);
48-
await next(context);
48+
await next(context).ConfigureAwait(false);
4949
return;
5050
}
5151

52-
var processed = await _store.ProcessAsync(key, context, _ => next(context), context.CancellationToken);
52+
var processed = await _store.ProcessAsync(key, context, _ => next(context), context.CancellationToken).ConfigureAwait(false);
5353

5454
if (processed)
5555
{

src/Vulthil.Messaging.Inbox/InboxRetentionBackgroundService.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
2828
using var timer = new PeriodicTimer(_options.SweepInterval, timeProvider);
2929
do
3030
{
31-
await SweepSafelyAsync(stoppingToken);
31+
await SweepSafelyAsync(stoppingToken).ConfigureAwait(false);
3232
}
33-
while (await timer.WaitForNextTickAsync(stoppingToken));
33+
while (await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false));
3434
}
3535
catch (OperationCanceledException exception) when (stoppingToken.IsCancellationRequested)
3636
{
@@ -48,7 +48,7 @@ private async Task SweepSafelyAsync(CancellationToken cancellationToken)
4848
{
4949
try
5050
{
51-
await SweepAsync(cancellationToken);
51+
await SweepAsync(cancellationToken).ConfigureAwait(false);
5252
}
5353
catch (Exception exception) when (exception is not OperationCanceledException || !cancellationToken.IsCancellationRequested)
5454
{
@@ -58,7 +58,8 @@ private async Task SweepSafelyAsync(CancellationToken cancellationToken)
5858

5959
private async Task SweepAsync(CancellationToken cancellationToken)
6060
{
61-
await using var scope = scopeFactory.CreateAsyncScope();
61+
var scope = scopeFactory.CreateAsyncScope();
62+
await using var _ = scope.ConfigureAwait(false);
6263
if (scope.ServiceProvider.GetRequiredService<IIdempotencyStore>() is not IInboxRetentionStore store)
6364
{
6465
if (!_loggedMissingRetentionStore)
@@ -76,7 +77,7 @@ private async Task SweepAsync(CancellationToken cancellationToken)
7677
int deleted;
7778
do
7879
{
79-
deleted = await store.DeleteProcessedAsync(cutoff, batchSize, cancellationToken);
80+
deleted = await store.DeleteProcessedAsync(cutoff, batchSize, cancellationToken).ConfigureAwait(false);
8081
total += deleted;
8182
}
8283
while (deleted >= batchSize && !cancellationToken.IsCancellationRequested);

src/Vulthil.Messaging.Outbox/BrokerOutboxDispatcher.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,14 @@ public async Task DispatchAsync(OutboxMessageData message, CancellationToken can
5555
{
5656
var address = new Uri(metadata?.DestinationAddress
5757
?? throw new InvalidOperationException("An outbox send message is missing its destination address."));
58-
var endpoint = await sendEndpointProvider.GetSendEndpointAsync(address, cancellationToken);
58+
var endpoint = await sendEndpointProvider.GetSendEndpointAsync(address, cancellationToken).ConfigureAwait(false);
5959
var send = SendByType.GetOrAdd(messageType, static type => SendMethod.MakeGenericMethod(type));
60-
await (Task)send.Invoke(endpoint, [payload, configure, cancellationToken])!;
60+
await ((Task)send.Invoke(endpoint, [payload, configure, cancellationToken])!).ConfigureAwait(false);
6161
}
6262
else
6363
{
6464
var publish = PublishByType.GetOrAdd(messageType, static type => PublishMethod.MakeGenericMethod(type));
65-
await (Task)publish.Invoke(publisher, [payload, configure, cancellationToken])!;
65+
await ((Task)publish.Invoke(publisher, [payload, configure, cancellationToken])!).ConfigureAwait(false);
6666
}
6767
}
6868

src/Vulthil.Messaging.Outbox/TransactionalConsumeFilter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public Task ConsumeAsync(IMessageContext<TMessage> context, ConsumeDelegate<TMes
1717
unitOfWork.ExecuteInTransactionAsync(
1818
async _ =>
1919
{
20-
await next(context);
20+
await next(context).ConfigureAwait(false);
2121
return true;
2222
},
2323
context.CancellationToken);

0 commit comments

Comments
 (0)