Skip to content

Add async responder overloads and CancellationToken flow#132

Merged
dennisdoomen merged 1 commit into
mainfrom
dennisdoomen/async-responder-pipeline
Jun 20, 2026
Merged

Add async responder overloads and CancellationToken flow#132
dennisdoomen merged 1 commit into
mainfrom
dennisdoomen/async-responder-pipeline

Conversation

@dennisdoomen

Copy link
Copy Markdown
Owner

Closes #120

Summary

Adds asynchronous responder support to Mockly and flows the previously-dropped CancellationToken through the mock pipeline. This is the foundation of an async-pipeline stack — issues #117 (latency) and #116 (simulated failures) will stack on top of this branch and build on the single async execution path introduced here.

What changed

  • One async execution path. RequestMock gains an internal async responder (Func<RequestInfo, CancellationToken, Task<HttpResponseMessage>>). The existing synchronous Responder is adapted onto this path so both sync and async responders converge in a new internal TrackRequestAsync.
  • CancellationToken is now honored. The token flows from MockHttpMessageHandler.SendAsyncHttpMock.HandleRequestRequestMock.TrackRequestAsync (it was previously ignored). OperationCanceledException propagates out of the pipeline instead of being swallowed into a 500.
  • Backward compatible. The public synchronous RequestMock.Responder property and RequestMock.TrackRequest(RequestInfo) method are unchanged and still work.

New public API

RequestMockResponseBuilder RespondsWith(Func<RequestInfo, Task<HttpResponseMessage>> responder);
RequestMockResponseBuilder RespondsWith(Func<RequestInfo, CancellationToken, Task<HttpResponseMessage>> responder);

The approved API verification files (Mockly.ApiVerificationTests/ApprovedApi/*.verified.txt) were regenerated via AcceptApiChanges.ps1 and committed.

Tests

New WhenUsingAsyncResponders spec class covers: async responder is awaited; async + CancellationToken overload is awaited; the token is passed to the responder; a cancelled token is observed and propagates; async responders work with invocation limits and request collection; exceptions surface as 500; and existing synchronous responders still work.

Compatibility note

Overloading RespondsWith on the delegate return type makes a throw-only lambda (RespondsWith(_ => throw ...)) ambiguous, requiring an explicit delegate cast. One existing test was updated accordingly. Lambdas with an actual return value remain unambiguous.

Verification

  • dotnet test green for both net8.0 (95 passed) and net472 (94 passed).
  • API verification tests green after accepting the additive changes.

Stacking

#117 and #116 will stack on this branch and layer latency/failure simulation onto the async responder pipeline. Opened as draft for maintainer review — do not merge yet.

@dennisdoomen dennisdoomen added the enhancement New feature or request label May 30, 2026
.RespondsWith(async (_, ct) =>
{
#if NET8_0_OR_GREATER
await cts.CancelAsync();
#if NET8_0_OR_GREATER
await cts.CancelAsync();
#else
cts.Cancel();
var client = mock.GetClient();

// Act
Func<Task> act = () => client.GetAsync("https://localhost/api/cancel", cts.Token);
{
// Arrange
var mock = new HttpMock();
CancellationToken observedToken = default;
var client = mock.GetClient();

// Act
await client.GetAsync("https://localhost/api/observe");
@github-actions

Copy link
Copy Markdown

Test Results

  3 files  ± 0    3 suites  ±0   6s ⏱️ ±0s
 97 tests + 8   97 ✅ + 8  0 💤 ±0  0 ❌ ±0 
191 runs  +16  191 ✅ +16  0 💤 ±0  0 ❌ ±0 

Results for commit e3fc9d6. ± Comparison against base commit 5239fa0.

@github-actions

github-actions Bot commented May 30, 2026

Copy link
Copy Markdown

Test Results

  3 files  ± 0    3 suites  ±0   6s ⏱️ -19s
167 tests +12  167 ✅ +12  0 💤 ±0  0 ❌ ±0 
331 runs  +24  331 ✅ +24  0 💤 ±0  0 ❌ ±0 

Results for commit 7df3f66. ± Comparison against base commit a09785e.

♻️ This comment has been updated with latest results.

@coveralls

coveralls commented May 30, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 27873525411

Coverage decreased (-0.7%) to 83.889%

Details

  • Coverage decreased (-0.7%) from the base build.
  • Patch coverage: 57 of 57 lines across 3 files are fully covered (100%).
  • 21 coverage regressions across 1 file.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

21 previously-covered lines in 1 file lost coverage.

File Lines Losing Coverage Coverage
Mockly/RequestMock.cs 21 80.3%

Coverage Stats

Coverage Status
Relevant Lines: 1376
Covered Lines: 1219
Line Coverage: 88.59%
Relevant Branches: 455
Covered Branches: 317
Branch Coverage: 69.67%
Branches in Coverage %: Yes
Coverage Strength: 285.86 hits per line

💛 - Coveralls

@dennisdoomen dennisdoomen marked this pull request as ready for review May 30, 2026 18:15
@dennisdoomen dennisdoomen changed the title Add async responder overloads and CancellationToken flow (closes #120) Add async responder overloads and CancellationToken flow May 30, 2026
Introduce an internal async responder path on RequestMock and converge the synchronous Responder onto a single async execution path (TrackRequestAsync). Thread the CancellationToken from MockHttpMessageHandler.SendAsync through HttpMock.HandleRequest into RequestMock. Add public RespondsWith overloads accepting Func<RequestInfo, Task<HttpResponseMessage>> and Func<RequestInfo, CancellationToken, Task<HttpResponseMessage>>. The existing synchronous Responder property and TrackRequest method are preserved.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dennisdoomen dennisdoomen force-pushed the dennisdoomen/async-responder-pipeline branch from e3fc9d6 to 7df3f66 Compare June 20, 2026 14:06
// Assert
capturedInfo.Should().NotBeNull();
capturedInfo.Method.Should().Be(HttpMethod.Post);
capturedInfo.Uri.AbsolutePath.Should().Be("/api/info");
var builder = mock.ForGet().WithPath("/api/null");

// Act
Action act = () => builder.RespondsWith((Func<RequestInfo, Task<HttpResponseMessage>>)null!);
var builder = mock.ForGet().WithPath("/api/null-ct");

// Act
Action act = () => builder.RespondsWith((Func<RequestInfo, CancellationToken, Task<HttpResponseMessage>>)null!);
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add async responder overloads and flow CancellationToken into responders

3 participants