Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- When modifying a public member, make sure to check the Public.API files for the affected assembly and update them if necessary.
- Do not ignore CS1591 warnings; analyze and add missing XML comments instead.
- 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.
- 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.

## Testing Guidelines
- Prefer using the Vulthil.xUnit testing framework for tests.
Expand Down
6 changes: 6 additions & 0 deletions src/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Policy: everything under src/ ships to NuGet as a library, so it must never capture the
# caller's synchronization context. CA2007 is an error here (overriding the repo-wide default
# in the root .editorconfig) so every await in shipped code calls ConfigureAwait(false).
# tests/ and samples/ are consumers, not libraries, and stay exempt.
[*.cs]
dotnet_diagnostic.CA2007.severity = error
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public static async Task<TResponse> GetResponseAsync<TResponse>(this HttpRespons
ArgumentNullException.ThrowIfNull(response);
response.EnsureSuccessStatusCode();

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

return result;
}
Expand Down
8 changes: 4 additions & 4 deletions src/Vulthil.Extensions.Testing/Polling.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public static async Task<PollingResult<T>> WaitAsync<T>(
{
do
{
Result<T> result = await func(linkedCts.Token);
Result<T> result = await func(linkedCts.Token).ConfigureAwait(false);

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

errors.Add(result.Error);
}
while (await timer.WaitForNextTickAsync(linkedCts.Token));
while (await timer.WaitForNextTickAsync(linkedCts.Token).ConfigureAwait(false));
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
{
Expand Down Expand Up @@ -234,7 +234,7 @@ public static async Task<PollingResult> WaitAsync(
{
do
{
Result result = await func(linkedCts.Token);
Result result = await func(linkedCts.Token).ConfigureAwait(false);

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

errors.Add(result.Error);
}
while (await timer.WaitForNextTickAsync(linkedCts.Token));
while (await timer.WaitForNextTickAsync(linkedCts.Token).ConfigureAwait(false));
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
{
Expand Down
12 changes: 6 additions & 6 deletions src/Vulthil.Messaging.Inbox.Cosmos/CosmosIdempotencyStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ public async Task<int> DeleteProcessedAsync(DateTimeOffset olderThanUtc, int bat
var markers = await dbContext.InboxMessages
.Where(marker => marker.ProcessedOnUtc < olderThanUtc)
.Take(batchSize)
.ToListAsync(cancellationToken);
.ToListAsync(cancellationToken).ConfigureAwait(false);

if (markers.Count == 0)
{
return 0;
}

dbContext.InboxMessages.RemoveRange(markers);
await SaveRemovedMarkersAsync(cancellationToken);
await SaveRemovedMarkersAsync(cancellationToken).ConfigureAwait(false);
return markers.Count;
}

Expand All @@ -46,7 +46,7 @@ private async Task SaveRemovedMarkersAsync(CancellationToken cancellationToken)
{
try
{
await dbContext.SaveChangesAsync(cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
catch (DbUpdateConcurrencyException exception)
{
Expand All @@ -67,12 +67,12 @@ public async Task<bool> ProcessAsync(string idempotencyKey, IMessageContext cont
ArgumentException.ThrowIfNullOrEmpty(idempotencyKey);
ArgumentNullException.ThrowIfNull(process);

if (await dbContext.InboxMessages.FindAsync([idempotencyKey], cancellationToken) is not null)
if (await dbContext.InboxMessages.FindAsync([idempotencyKey], cancellationToken).ConfigureAwait(false) is not null)
{
return false;
}

await process(cancellationToken);
await process(cancellationToken).ConfigureAwait(false);

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

try
{
await dbContext.SaveChangesAsync(cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
return true;
}
catch (DbUpdateException exception) when (IsConflict(exception))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public async Task<int> DeleteProcessedAsync(DateTimeOffset olderThanUtc, int bat
.OrderBy(marker => marker.ProcessedOnUtc)
.Take(batchSize)
.Select(marker => marker.MessageId)
.ToListAsync(cancellationToken);
.ToListAsync(cancellationToken).ConfigureAwait(false);

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

return await dbContext.InboxMessages
.Where(marker => keys.Contains(marker.MessageId))
.ExecuteDeleteAsync(cancellationToken);
.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
}

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

var strategy = dbContext.Database.CreateExecutionStrategy();
return strategy.ExecuteAsync(
async token => await ProcessInOwnTransactionAsync(idempotencyKey, process, token),
async token => await ProcessInOwnTransactionAsync(idempotencyKey, process, token).ConfigureAwait(false),
cancellationToken);
}

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

if (await HasProcessedAsync(idempotencyKey, cancellationToken))
if (await HasProcessedAsync(idempotencyKey, cancellationToken).ConfigureAwait(false))
{
return false;
}

await process(cancellationToken);
await process(cancellationToken).ConfigureAwait(false);
AddMarker(idempotencyKey);

try
{
await dbContext.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
return true;
}
catch (DbUpdateException)
{
await transaction.RollbackAsync(cancellationToken);
await transaction.RollbackAsync(cancellationToken).ConfigureAwait(false);

if (await HasProcessedAsync(idempotencyKey, cancellationToken))
if (await HasProcessedAsync(idempotencyKey, cancellationToken).ConfigureAwait(false))
{
return false;
}
Expand All @@ -95,14 +96,14 @@ private async Task<bool> ProcessInOwnTransactionAsync(string idempotencyKey, Fun

private async Task<bool> ProcessInAmbientTransactionAsync(string idempotencyKey, Func<CancellationToken, Task> process, CancellationToken cancellationToken)
{
if (await HasProcessedAsync(idempotencyKey, cancellationToken))
if (await HasProcessedAsync(idempotencyKey, cancellationToken).ConfigureAwait(false))
{
return false;
}

await process(cancellationToken);
await process(cancellationToken).ConfigureAwait(false);
AddMarker(idempotencyKey);
await dbContext.SaveChangesAsync(cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
return true;
}

Expand Down
4 changes: 2 additions & 2 deletions src/Vulthil.Messaging.Inbox/IdempotentConsumeFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ public async Task ConsumeAsync(IMessageContext<TMessage> context, ConsumeDelegat

InboxLog.MissingKeyAllowed(_logger, _messageType);
InboxTelemetry.MissingKey.Add(1);
await next(context);
await next(context).ConfigureAwait(false);
return;
}

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

if (processed)
{
Expand Down
11 changes: 6 additions & 5 deletions src/Vulthil.Messaging.Inbox/InboxRetentionBackgroundService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
using var timer = new PeriodicTimer(_options.SweepInterval, timeProvider);
do
{
await SweepSafelyAsync(stoppingToken);
await SweepSafelyAsync(stoppingToken).ConfigureAwait(false);
}
while (await timer.WaitForNextTickAsync(stoppingToken));
while (await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false));
}
catch (OperationCanceledException exception) when (stoppingToken.IsCancellationRequested)
{
Expand All @@ -48,7 +48,7 @@ private async Task SweepSafelyAsync(CancellationToken cancellationToken)
{
try
{
await SweepAsync(cancellationToken);
await SweepAsync(cancellationToken).ConfigureAwait(false);
}
catch (Exception exception) when (exception is not OperationCanceledException || !cancellationToken.IsCancellationRequested)
{
Expand All @@ -58,7 +58,8 @@ private async Task SweepSafelyAsync(CancellationToken cancellationToken)

private async Task SweepAsync(CancellationToken cancellationToken)
{
await using var scope = scopeFactory.CreateAsyncScope();
var scope = scopeFactory.CreateAsyncScope();
await using var _ = scope.ConfigureAwait(false);
if (scope.ServiceProvider.GetRequiredService<IIdempotencyStore>() is not IInboxRetentionStore store)
{
if (!_loggedMissingRetentionStore)
Expand All @@ -76,7 +77,7 @@ private async Task SweepAsync(CancellationToken cancellationToken)
int deleted;
do
{
deleted = await store.DeleteProcessedAsync(cutoff, batchSize, cancellationToken);
deleted = await store.DeleteProcessedAsync(cutoff, batchSize, cancellationToken).ConfigureAwait(false);
total += deleted;
}
while (deleted >= batchSize && !cancellationToken.IsCancellationRequested);
Expand Down
6 changes: 3 additions & 3 deletions src/Vulthil.Messaging.Outbox/BrokerOutboxDispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,14 @@ public async Task DispatchAsync(OutboxMessageData message, CancellationToken can
{
var address = new Uri(metadata?.DestinationAddress
?? throw new InvalidOperationException("An outbox send message is missing its destination address."));
var endpoint = await sendEndpointProvider.GetSendEndpointAsync(address, cancellationToken);
var endpoint = await sendEndpointProvider.GetSendEndpointAsync(address, cancellationToken).ConfigureAwait(false);
var send = SendByType.GetOrAdd(messageType, static type => SendMethod.MakeGenericMethod(type));
await (Task)send.Invoke(endpoint, [payload, configure, cancellationToken])!;
await ((Task)send.Invoke(endpoint, [payload, configure, cancellationToken])!).ConfigureAwait(false);
}
else
{
var publish = PublishByType.GetOrAdd(messageType, static type => PublishMethod.MakeGenericMethod(type));
await (Task)publish.Invoke(publisher, [payload, configure, cancellationToken])!;
await ((Task)publish.Invoke(publisher, [payload, configure, cancellationToken])!).ConfigureAwait(false);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Vulthil.Messaging.Outbox/TransactionalConsumeFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public Task ConsumeAsync(IMessageContext<TMessage> context, ConsumeDelegate<TMes
unitOfWork.ExecuteInTransactionAsync(
async _ =>
{
await next(context);
await next(context).ConfigureAwait(false);
return true;
},
context.CancellationToken);
Expand Down
4 changes: 2 additions & 2 deletions src/Vulthil.Messaging.Outbox/TransactionalPublishFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ public async Task PublishAsync(PublishFilterContext context, PublishFilterDelega
"establish the transaction instead.");
}

await next(context);
await next(context).ConfigureAwait(false);
return;
}

outboxStore.AddOutboxMessage(CreateRow(context));

await outboxStore.SaveChangesAsync(context.CancellationToken);
await outboxStore.SaveChangesAsync(context.CancellationToken).ConfigureAwait(false);
}

private OutboxMessage CreateRow(PublishFilterContext context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public static MessageHandler ForConsumer<TConsumer, TMessage>(RetryPolicyDefinit
sp,
terminal: c => consumer.ConsumeAsync(c, c.CancellationToken));

await pipeline(context);
await pipeline(context).ConfigureAwait(false);
}
};

Expand Down Expand Up @@ -79,11 +79,11 @@ public static MessageHandler ForRequestConsumer<TConsumer, TRequest, TResponse>(
sp,
terminal: async c =>
{
response = await consumer.ConsumeAsync(c, c.CancellationToken);
response = await consumer.ConsumeAsync(c, c.CancellationToken).ConfigureAwait(false);
responseProduced = true;
});

await pipeline(context);
await pipeline(context).ConfigureAwait(false);

reply = responseProduced
? BuildReply(provider.GetUrn(typeof(TResponse)), JsonSerializer.SerializeToElement(response, jsonOptions), ea, envelope)
Expand All @@ -100,7 +100,7 @@ public static MessageHandler ForRequestConsumer<TConsumer, TRequest, TResponse>(
reply = BuildFaultReply(exception.Message, exception.GetType().FullName ?? "Unknown", exception.StackTrace, jsonOptions, ea, envelope);
}

await SendResponseAsync(ea, reply, publishAsync, jsonOptions);
await SendResponseAsync(ea, reply, publishAsync, jsonOptions).ConfigureAwait(false);
}
};

Expand Down Expand Up @@ -148,6 +148,6 @@ private static async Task SendResponseAsync(BasicDeliverEventArgs ea, MessageEnv
ContentType = RabbitMqConstants.ContentType,
};

await publishAsync(string.Empty, ea.BasicProperties.ReplyTo, true, replyProps, body);
await publishAsync(string.Empty, ea.BasicProperties.ReplyTo, true, replyProps, body).ConfigureAwait(false);
}
}
Loading