WireMock.Net: HttpClient extension methods causes ambiguous invocations in .NET 7

Describe the bug

When adding WireMock to a .NET 7 project, it breaks the usage of certain HttpClient extension methods by causing ambiguous invocations.

Expected behavior:

There should be no ambiguous invocations/conflicts when using WireMock.

Test to reproduce

  1. Create a new .NET 7 class library.
  2. Add the following to the Class1.cs file:
using System.Net.Http.Json;

namespace ClassLibrary1;

public class Class1
{
    public void Foo()
    {
        var httpClient = new HttpClient();
        httpClient.PostAsJsonAsync("test", new { Foo = "bar" }, CancellationToken.None);
    }
}
  1. Build the project and confirm there are no compilation errors.
  2. Add a package reference to WireMock (at this time the latest version is 1.5.32).
<ItemGroup>
    <PackageReference Include="WireMock.Net" Version="1.5.32" />
</ItemGroup>
  1. Build the project and observe the compilation error about the ambiguous invocation on PostAsJsonAsync.

Other related info

There might be other extension methods that cause the same issue. Removing the cancellation token from the PostAsJsonAsync call will resolve the extension method located in the seemingly deprecated System.Net.Http.Formatting assembly, which seems to originate from the Microsoft.AspNet.WebApi.Client package according to Visual Studio. image

This resolves to System.Net.Http.HttpClientExtensions (System.Net.Http.Formatting.dll):

httpClient.PostAsJsonAsync("test", new { Foo = "bar" });

This resolves to System.Net.Http.Json.HttpClientJsonExtensions (System.Net.Http.Json.dll):

httpClient.PostAsJsonAsync("test", new { Foo = "bar" }, JsonSerializerOptions.Default, CancellationToken.None);

Potential workaround

To resolve the ambiguity, it is possible to invoke the extension method as a static method instead, but this is not ideal and will require you to remember this issue in order to not end up in this situation again.

HttpClientJsonExtensions.PostAsJsonAsync(httpClient, "test", new { Foo = "bar" }, CancellationToken.None);

Another solution is to give the System.Net.Http.Formatting assembly a different alias (see this StackOverflow answer)

<Target Name="ChangeAliasOfSystemNetHttpFormatting" BeforeTargets="FindReferenceAssembliesForReferences;ResolveReferences">
  <ItemGroup>
    <ReferencePath Condition="'%(FileName)' == 'System.Net.Http.Formatting'">
      <Aliases>nonmerged</Aliases>
    </ReferencePath>
  </ItemGroup>
</Target>

This is probably the nicest way to solve it, but it’s still something that’s less than ideal.

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 17 (9 by maintainers)

Most upvoted comments

Yes, the preview version works without the workaround 👍

I had a look at the implementation in the HttpClientFactory and I believe this should be able to do the same without the need for the package dependency:

using System.Linq;
using System.Net.Http;

namespace WireMock.Http;

internal static class HttpClientFactory2
{
    public static HttpClient Create(params DelegatingHandler[] handlers)
    {
        var handler = CreateHandlerPipeline(new HttpClientHandler(), handlers);
        return new HttpClient(handler);
    }

    public static HttpClient Create(HttpMessageHandler innerHandler, params DelegatingHandler[] handlers)
    {
        var handler = CreateHandlerPipeline(innerHandler, handlers);
        return new HttpClient(handler);
    }

    private static HttpMessageHandler CreateHandlerPipeline(HttpMessageHandler handler, params DelegatingHandler[] delegatingHandlers)
    {
        if (delegatingHandlers.Length == 0)
        {
            return handler;
        }

        var next = handler;
        
        foreach (var delegatingHandler in delegatingHandlers.Reverse())
        {
            delegatingHandler.InnerHandler = next;
            next = delegatingHandler;
        }

        return next;
    }
}