HttpClient extension methods causes ambiguous invocations in .NET 7 #532

Closed
opened 2025-12-29 15:26:05 +01:00 by adam · 17 comments
Owner

Originally created by @siewers on GitHub (Jul 19, 2023).

Originally assigned to: @StefH on GitHub.

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.

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.

Originally created by @siewers on GitHub (Jul 19, 2023). Originally assigned to: @StefH on GitHub. ### 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: ```cs 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); } } ``` 3. Build the project and confirm there are no compilation errors. 4. Add a package reference to WireMock (at this time the latest version is 1.5.32). ```xml <ItemGroup> <PackageReference Include="WireMock.Net" Version="1.5.32" /> </ItemGroup> ``` 5. 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`](https://learn.microsoft.com/en-us/previous-versions/aspnet/hh944845(v=vs.118)) assembly, which seems to originate from the `Microsoft.AspNet.WebApi.Client` package according to Visual Studio. ![image](https://github.com/WireMock-Net/WireMock.Net/assets/226147/f9634214-356f-484f-b154-a637898a30ea) This resolves to `System.Net.Http.HttpClientExtensions` (System.Net.Http.Formatting.dll): ```cs httpClient.PostAsJsonAsync("test", new { Foo = "bar" }); ``` This resolves to `System.Net.Http.Json.HttpClientJsonExtensions` (System.Net.Http.Json.dll): ```cs 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. ```cs 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](https://stackoverflow.com/a/52252705/76048)) ```xml <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.
adam added the bug label 2025-12-29 15:26:05 +01:00
adam closed this issue 2025-12-29 15:26:05 +01:00
Author
Owner

@StefH commented on GitHub (Jul 20, 2023):

@siewers
I applied your last suggestion.

Can you review PR https://github.com/WireMock-Net/WireMock.Net/pull/977 ?

@StefH commented on GitHub (Jul 20, 2023): @siewers I applied your last suggestion. Can you review PR https://github.com/WireMock-Net/WireMock.Net/pull/977 ?
Author
Owner

@StefH commented on GitHub (Jul 21, 2023):

Can you try preview version 1.5.32-ci-17648 ?

https://github.com/WireMock-Net/WireMock.Net/wiki/MyGet-preview-versions

@StefH commented on GitHub (Jul 21, 2023): Can you try preview version 1.5.32-ci-17648 ? https://github.com/WireMock-Net/WireMock.Net/wiki/MyGet-preview-versions
Author
Owner

@siewers commented on GitHub (Jul 21, 2023):

Unfortunately, that won't work.
The reason is that the package is transitively available to all consumers of WireMock.Net. What this change does is only aliasing it within the WireMock.Net project, but not for consumers.
Since you aren't referencing the package directly in WireMock.Net, it's not possible to hide that one directly from consumers, but perhaps there is a trick to mark it with PrivateAssets="compile;build;analyzers" somehow, so it won't be accessible to consumers.
It might even be possible to hide the Microsoft.AspNet.WebApi.Client package reference like so:

<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.8" PrivateAssets="compile;build;analyzers" />

I believe (but I'm not sure) that hiding a package reference like this also hides all its transitive dependencies, but I would need to test it.

In general, I think WireMock exposes too many of its internal dependencies, resulting in IntelliSense becoming extremely bloated with all kinds of references not included in the consuming project itself.
I believe it's good practice to hide the library dependencies to consumers to not unnecessarily bloat the IntelliSense and accidentally start relying on a dependency not directly referenced (e.g. the GraphQL package that exposes a lot of extension methods as well).
If you ever decide to remove or upgrade such a dependency, you might inadvertently break consumers of WireMock.Net, because they in turn relied on some random extension method from a package you included, which is now removed or has changed.

I know there are probably a lot of opinions in this area, but it's risky exposing dependencies not intended to be used by consumers.

On the other hand, there's also the risk of package version mismatch, if the consumer directly references a package that WireMock.Net depends on, but hides. In this case, one way to solve it could be to add a version range in the package reference, allowing consumers to depend on e.g. Newtonsoft.Json version 12 through 18 or something like that. If the consumer upgrades the package to version 19, the project won't build because WireMock.Net isn't tested or guaranteed compatible with that version.

It's a lot of complexity for such a seemingly tiny problem, but it's definitely worth considering for future maintainability.

Oh, one more thing, if you decide to hide all internal dependencies, it's most likely a major breaking change, as nobody knows what packages are being used transitively by consumers.

@siewers commented on GitHub (Jul 21, 2023): Unfortunately, that won't work. The reason is that the package is transitively available to all consumers of WireMock.Net. What this change does is only aliasing it within the WireMock.Net project, but not for consumers. Since you aren't referencing the package directly in WireMock.Net, it's not possible to hide that one directly from consumers, but perhaps there is a trick to mark it with `PrivateAssets="compile;build;analyzers"` somehow, so it won't be accessible to consumers. It might even be possible to hide the `Microsoft.AspNet.WebApi.Client` package reference like so: ```xml <PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.8" PrivateAssets="compile;build;analyzers" /> ``` I believe (but I'm not sure) that hiding a package reference like this also hides all its transitive dependencies, but I would need to test it. In general, I think WireMock exposes too many of its internal dependencies, resulting in IntelliSense becoming extremely bloated with all kinds of references not included in the consuming project itself. I believe it's good practice to hide the library dependencies to consumers to not unnecessarily bloat the IntelliSense and accidentally start relying on a dependency not directly referenced (e.g. the GraphQL package that exposes a lot of extension methods as well). If you ever decide to remove or upgrade such a dependency, you might inadvertently break consumers of WireMock.Net, because they in turn relied on some random extension method from a package you included, which is now removed or has changed. I know there are probably a lot of opinions in this area, but it's risky exposing dependencies not intended to be used by consumers. On the other hand, there's also the risk of package version mismatch, if the consumer directly references a package that WireMock.Net depends on, but hides. In this case, one way to solve it could be to add a [version range](https://learn.microsoft.com/en-us/nuget/concepts/package-versioning#version-ranges) in the package reference, allowing consumers to depend on e.g. `Newtonsoft.Json` version 12 through 18 or something like that. If the consumer upgrades the package to version 19, the project won't build because WireMock.Net isn't tested or guaranteed compatible with that version. It's a lot of complexity for such a seemingly tiny problem, but it's definitely worth considering for future maintainability. Oh, one more thing, _if_ you decide to hide all internal dependencies, it's most likely a major breaking change, as nobody knows what packages are being used transitively by consumers.
Author
Owner

@StefH commented on GitHub (Jul 21, 2023):

Thank you for your detailed explanation and suggestions.
I think that marking the dependencies as PrivateAssets is the best solution in the long term which indeed could mean a breaking change.

(https://learn.microsoft.com/en-us/answers/questions/702182/how-to-hide-dependent-nuget-dlls-from-consuming-pa)

For now I've deleted the PR.

Another approach which I was trying to build for this specific issue is removing the dependency on Microsoft.AspNet.WebApi.Client (which I use for creating a httpclient by using a httpclientfactory) for .NET 6 and higher. I did build a work-around using code like this:

public class X
{
	public static X Instance = new X();
	
	public readonly IHttpClientFactory Factory;
	
	private X()
	{
		var services = new ServiceCollection();
		services.AddHttpClient("WireMock.Net.HttpClient");
		
		Factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();
	}
}

This worked, however the current code uses:

public HttpClient CreateClient(params DelegatingHandler[] handlers)

And the handlers are not supported in that same way....

I need to think on that some more...

@StefH commented on GitHub (Jul 21, 2023): Thank you for your detailed explanation and suggestions. I think that marking the dependencies as PrivateAssets is the best solution in the long term which indeed could mean a breaking change. (https://learn.microsoft.com/en-us/answers/questions/702182/how-to-hide-dependent-nuget-dlls-from-consuming-pa) For now I've deleted the PR. Another approach which I was trying to build for this specific issue is removing the dependency on Microsoft.AspNet.WebApi.Client (which I use for creating a httpclient by using a httpclientfactory) for .NET 6 and higher. I did build a work-around using code like this: ``` c# public class X { public static X Instance = new X(); public readonly IHttpClientFactory Factory; private X() { var services = new ServiceCollection(); services.AddHttpClient("WireMock.Net.HttpClient"); Factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>(); } } ``` This worked, however the current code uses: ``` c# public HttpClient CreateClient(params DelegatingHandler[] handlers) ``` And the handlers are not supported in that same way.... I need to think on that some more...
Author
Owner

@siewers commented on GitHub (Jul 22, 2023):

You may want to have a look at the documentation here https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory#configure-the-httpmessagehandler

The pattern is to inject a typed, configured, client into X and then use that one instead of the factory directly. You can add handlers when you configure an HttpClient using AddHttpClient<TService, TImplementation>.
When you then resolve X you would have to do so as either transitive or scoped. This approach is not suited for singletons.
I don’t know how important that is, but your instance property could technically be changed into something like a factory instead, where you then resolve a new instance each time.

I haven’t checked for .NET 6 and above, but at some point there was also a static factory method on the ‘HttpClientFactory` class, I’m just not sure if that exists anymore.

@siewers commented on GitHub (Jul 22, 2023): You may want to have a look at the documentation here https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory#configure-the-httpmessagehandler The pattern is to inject a typed, configured, client into `X` and then use that one instead of the factory directly. You can add handlers when you configure an HttpClient using `AddHttpClient<TService, TImplementation>`. When you then resolve `X` you would have to do so as either transitive or scoped. This approach is not suited for singletons. I don’t know how important that is, but your instance property could technically be changed into something like a factory instead, where you then resolve a new instance each time. I haven’t checked for .NET 6 and above, but at some point there was also a static factory method on the ‘HttpClientFactory` class, I’m just not sure if that exists anymore.
Author
Owner

@siewers commented on GitHub (Jul 25, 2023):

What I was thinking was something along these lines could work as well:

public class X
{
    private static readonly ServiceProvider _serviceProvider;
    
    private readonly HttpClient _httpClient;
    
    static X()
    {
        var services = new ServiceCollection();
        services.AddScoped<MyHandler>();
        // If X has a public constructor that accepts an HttpClient, it can be injected by using AddHttpClient<X, X>() instead of a named instance like this, although there shouldn't be any technical difference.
        services.AddHttpClient<X>("MyHttpClient")
                .AddHttpMessageHandler<MyHandler>()
                .SetHandlerLifetime(TimeSpan.FromMinutes(5));
        _serviceProvider = services.BuildServiceProvider();  
    }
    
    private X(IHttpClientFactory httpClientFactory)
    {
        // With a public constructor, you can simply inject the HttpClient directly
        _httpClient = httpClientFactory.CreateClient("MyHttpClient");
    }

    public static X CreateInstance()
    {
        // It is necessary to create the instance of X if the constructor needs to be private.
        // If the constructor was public, you could just return serviceProvider.GetRequiredService<X>();
        return new X(_serviceProvider.GetRequiredService<IHttpClientFactory>());
    }
}

internal sealed class MyHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

One concern I have with this approach is the fact that the HttpClient never gets disposed and the ServiceProvider lives on and might leak somehow. I'm not familiar with how this class would be used, so I'm not sure this is the best approach.

It is possible to make the class disposable and then simply dispose the service collection at some point, but again, I don't know the exact use-case for this.

@siewers commented on GitHub (Jul 25, 2023): What I was thinking was something along these lines could work as well: ```cs public class X { private static readonly ServiceProvider _serviceProvider; private readonly HttpClient _httpClient; static X() { var services = new ServiceCollection(); services.AddScoped<MyHandler>(); // If X has a public constructor that accepts an HttpClient, it can be injected by using AddHttpClient<X, X>() instead of a named instance like this, although there shouldn't be any technical difference. services.AddHttpClient<X>("MyHttpClient") .AddHttpMessageHandler<MyHandler>() .SetHandlerLifetime(TimeSpan.FromMinutes(5)); _serviceProvider = services.BuildServiceProvider(); } private X(IHttpClientFactory httpClientFactory) { // With a public constructor, you can simply inject the HttpClient directly _httpClient = httpClientFactory.CreateClient("MyHttpClient"); } public static X CreateInstance() { // It is necessary to create the instance of X if the constructor needs to be private. // If the constructor was public, you could just return serviceProvider.GetRequiredService<X>(); return new X(_serviceProvider.GetRequiredService<IHttpClientFactory>()); } } internal sealed class MyHandler : DelegatingHandler { protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { throw new NotImplementedException(); } } ``` One concern I have with this approach is the fact that the HttpClient never gets disposed and the ServiceProvider lives on and might leak somehow. I'm not familiar with how this class would be used, so I'm not sure this is the best approach. It is possible to make the class disposable and then simply dispose the service collection at some point, but again, I don't know the exact use-case for this.
Author
Owner

@StefH commented on GitHub (Jul 25, 2023):

I was also checking this
https://github.com/myarichuk/Simple.HttpClientFactory

@StefH commented on GitHub (Jul 25, 2023): I was also checking this https://github.com/myarichuk/Simple.HttpClientFactory
Author
Owner

@siewers commented on GitHub (Jul 25, 2023):

Well, it's an option, although I'm having a hard time finding it justifiable today.
Sure, you save a few lines of internal implementation code, but the project seems stale and haven't had an update for three years and only 8 stars. The rationale behind the project seems to be a bit of a stretch IMHO :)
I'd suggest sticking with the framework and not pull in other third-party packages to solve this problem :)

@siewers commented on GitHub (Jul 25, 2023): Well, it's an option, although I'm having a hard time finding it justifiable today. Sure, you save a few lines of internal implementation code, but the project seems stale and haven't had an update for three years and only 8 stars. The rationale behind the project seems to be a bit of a stretch IMHO :) I'd suggest sticking with the framework and not pull in other third-party packages to solve this problem :)
Author
Owner

@siewers commented on GitHub (Jul 25, 2023):

I found this implementation of a framework "agnostic" HttpClientFactory that might be useful.
I've never worked with HTTP handler pipelines like this, so I'm not sure exactly how it should work, but from the looks of it, it seems pretty straight-forward.

@siewers commented on GitHub (Jul 25, 2023): I found [this implementation](https://github.com/arnath/standalone-httpclientfactory/blob/master/Arnath.StandaloneHttpClientFactory/StandaloneHttpClientFactory.cs) of a framework "agnostic" HttpClientFactory that might be useful. I've never worked with HTTP handler pipelines like this, so I'm not sure exactly how it should work, but from the looks of it, it seems pretty straight-forward.
Author
Owner

@siewers commented on GitHub (Aug 23, 2023):

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;
    }
}
@siewers commented on GitHub (Aug 23, 2023): I had a look at the implementation in the [`HttpClientFactory`](https://github.com/aspnet/AspNetWebStack/blob/main/src/System.Net.Http.Formatting/HttpClientFactory.cs) and I believe this should be able to do the same without the need for the package dependency: ```cs 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; } } ```
Author
Owner

@StefH commented on GitHub (Aug 25, 2023):

https://github.com/WireMock-Net/WireMock.Net/pull/996

@StefH commented on GitHub (Aug 25, 2023): https://github.com/WireMock-Net/WireMock.Net/pull/996
Author
Owner

@StefH commented on GitHub (Aug 26, 2023):

Can you try preview: 1.5.32-ci-17765

https://github.com/WireMock-Net/WireMock.Net/wiki/MyGet-preview-versions

@StefH commented on GitHub (Aug 26, 2023): Can you try preview: 1.5.32-ci-17765 https://github.com/WireMock-Net/WireMock.Net/wiki/MyGet-preview-versions
Author
Owner

@siewers commented on GitHub (Aug 28, 2023):

I can't find the build on MyGet. The only prerelease versions available are 1.5.34 and 1.5.35.

@siewers commented on GitHub (Aug 28, 2023): I can't find the build on MyGet. The only prerelease versions available are 1.5.34 and 1.5.35.
Author
Owner

@StefH commented on GitHub (Aug 28, 2023):

Did you select the preview select box in visual studio?

@StefH commented on GitHub (Aug 28, 2023): Did you select the preview select box in visual studio?
Author
Owner

@siewers commented on GitHub (Aug 28, 2023):

Yes, I am using the preview feed. The preview versions I see are not 1.5.32.

@siewers commented on GitHub (Aug 28, 2023): Yes, I am using the preview feed. The preview versions I see are not 1.5.32.
Author
Owner

@StefH commented on GitHub (Aug 29, 2023):

Version 1.5.32-ci-17775 should be visible on MyGet now.

@StefH commented on GitHub (Aug 29, 2023): Version 1.5.32-ci-17775 should be visible on MyGet now.
Author
Owner

@siewers commented on GitHub (Aug 29, 2023):

Yes, the preview version works without the workaround 👍

@siewers commented on GitHub (Aug 29, 2023): Yes, the preview version works without the workaround :+1:
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/WireMock.Net-wiremock#532