Skip to content

Freeze a Shared HttpMessageHandler

Problem

A system under test wraps HttpMessageHandler inside its own HttpClient internally — you want one composed, canned-response handler, and to be certain the system under test received that exact instance, not a look-alike, without reaching for a mocking library at all.

Solution

public sealed class StubHttpMessageHandler : HttpMessageHandler
{
    public StubHttpMessageHandler(HttpStatusCode statusCode)
    {
        Response = new HttpResponseMessage(statusCode);
    }

    public HttpResponseMessage Response { get; }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
        Task.FromResult(Response);
}

public sealed class OrderApiClient
{
    private readonly HttpClient _client;

    public OrderApiClient(StubHttpMessageHandler handler)
    {
        _client = new HttpClient(handler) { BaseAddress = new Uri("https://example.test") };
    }

    public Task<HttpResponseMessage> GetOrdersAsync() => _client.GetAsync("/orders");
}
[Theory]
[Compose]
public async Task GetOrdersAsync_ReturnsTheHandlersResponse([Shared] StubHttpMessageHandler handler, OrderApiClient client)
{
    // handler is the exact StubHttpMessageHandler instance OrderApiClient's own constructor received
    var response = await client.GetOrdersAsync();

    response.Should().BeSameAs(handler.Response);
}

[Shared] marks handler so OrderApiClient's own constructor dependency reuses this exact instance instead of composing a second, unrelated one — sharing is type-keyed and exact, so OrderApiClient's constructor parameter is declared as StubHttpMessageHandler (not the base HttpMessageHandler) to match (see Shared Values).

Discussion

StubHttpMessageHandler composes like any other concrete type — its HttpStatusCode constructor parameter comes from Compono's built-in enum provider, no Compono.NSubstitute needed. HttpClient itself is never composed directly: it has more than one public constructor, which is an ambiguous construction path Compono reports at compile time (CMP0001 — see Common Errors) rather than guessing which one to use. Wrapping it inside a type with a single constructor, like OrderApiClient above, is the fix — the same pattern applies to any BCL type with more than one accessible constructor.

Want the substitute-based version instead (asserting a specific request was sent, not just a canned response)? Compose the abstract HttpMessageHandler itself via Compono.NSubstitute (UseNSubstitute()), then configure its protected SendAsync through NSubstitute's Protected() helper.

See also