DEV Community

Bojan Nikolić
Bojan Nikolić

Posted on • Updated on

Make SOAP requests using IHttpClientFactory in .NET Core

SOAP services became second class citizen in .NET Core. REST was "the-way-to-go" and who creates SOAP services these days anyways? Well, maybe not these days, but a lot of them were created back in the days when SOAP was the King. Some of them are still alive and you need to access them.

Sure, accessing is not a big deal, VS still supports this, but if you worked with IHttpClientFactory when calling REST services and liked it, you would want to have all that while working with SOAP.

Is this possible?

Yes it is! You can find sample application here on my GitHub repository Browse through the code to figure what I did or just continue reading this article.

Problem and digging out the solution

Around a year and a half ago we moved our first service from "old" Framework to .NET Core. Later that year I started looking for a way to move to .NET Core some services that call SOAP 3rd party services. I wasn't satisfied with the basic stuff Core offers and I wanted something more similar to the options we have with IHttpClientFactory. Actually, I wanted the same experience!

I started looking for a solution. There were some custom libraries, but that was out of the question. They also felt hacky. I played around writing ISoapClientFactory as a copy of HTTP one, but only to realize I do not want to maintain something like that. At last, I postponed this for later.

Few months back, I gave it another go. This time I ran into GitHub issue that pointed me to both this blog post and this PR added to Core by a demigod of WCF - Matt Connew. Big thank you for these people!

So, is this article more/less a re-chewing of the GitHub Issue, PR and the blog post? Yes, you can say that, but I will explain one or two other walls I hit with this, so it is up to you where you will continue reading.

Add SOAP service

Adding existing SOAP/WCF service should not be a big issue. You should just follow the official documentation here

Caveat #1

What I encountered is that I couldn't do it. I kept getting the error that tool can't work on my .NET Core 3.x (I removed all previous versions) and requires v2.1. In order not to be blocked, I managed to find and install v2.1. Runtime installation should do the trick (I am not 100% sure, it was 2 months ago, do not hunt me down if runtime doesn't work and you need SDK). Visual Studio was now creating proxy as it should. Great!

How to make SOAP client work with IHttpClientFactory

When I went through those GitHub issue and PR, it is quite simple to answer - you need to add new Endpoint Behavior that will basically replace the default HttpClientHandler with the HttpMessageHandler that will be created by our beloved IHttpMessageHandlerFactory.

OK, how do I do that? First, you will need a new class that will implement IEndpointBehavior and will look like this

public class HttpMessageHandlerBehavior : IEndpointBehavior
    private readonly Func<HttpMessageHandler> _httpMessageHandler;

    public HttpMessageHandlerBehavior(IHttpMessageHandlerFactory factory, string serviceName)
        // Here we prescribe how handler will be created.
        // Since it uses IHttpMessageHandlerFactory, this factory will manage the setup and lifetime of the handler, 
        // based on the configuration we provided with AddHttpClient(serviceName) 
        _httpMessageHandler = () => factory.CreateHandler(serviceName);

    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
        // We need this line to add our HttpMessageHandler as HttpClientHandler.
        bindingParameters.Add(new Func<HttpClientHandler, HttpMessageHandler>(handler => _httpMessageHandler()));

    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { }

    public void Validate(ServiceEndpoint endpoint) { }        

Enter fullscreen mode Exit fullscreen mode

There you have all the "magic" where constructor accepts IHttpMessageHandlerFactory and defines function for creating a new handler; plus the part where we define how newly created HttpMessageHandler should be used as HttpClientHandler when calling SOAP service.

Of course, I had to add this EndpointBehavior in constructor of my HelloService singleton that injects IHttpMessageHandlerFactory

public HelloService(IHttpMessageHandlerFactory factory, ILogger<HelloService> logger)
    _logger = logger;
    _client = new Hello_PortTypeClient();
    _client.Endpoint.EndpointBehaviors.Add(new HttpMessageHandlerBehavior(factory, ServiceName));
Enter fullscreen mode Exit fullscreen mode

Wait! What is this ServiceName? Well, I need to configure HttpClient, so I will refer it with a ServiceName both in HelloService class and in Startup

// Here we configure how the HttpClient with HtpMessagehandler will be configured, like for any HTTP client (e.g. calling REST/JSON service)
services.AddHttpClient(HelloService.ServiceName, config =>
    // Some custom configuration like request timeout
    config.Timeout = TimeSpan.FromSeconds(5);
Enter fullscreen mode Exit fullscreen mode

There you will put your additional handlers, Polly resilience and all those nice things, like you already do when calling REST.

Caveat #2

You can't put in AddHttpClient stuff like BaseAddress or Authentication etc. That stuff needs to be setup on Endpoint or Binding, like explained in this blog post

I suggest you setup some of the things in constructor of your service (HelloService in my example) with injecting of Options, but I will leave that up to you, depending on how much things you need to setup and your best practices on that.

Caveat #3

I was so happy with the solution, so I just wanted to confirm it like in blog post with some load test with JMeter. I started the SoapUI mock (provided in the repository), opened CurrPorts and I didn't like what I found there. Even though calls did work, application opened too many connections - no connection pooling! Basically opened a connection per request! That was unacceptable.

It looked like solution was not using IHttpClientFactory. That was not possible, I went through debugger, it should work as advertised. CurrPorts was looking good on that blog post. What am I missing?

After few hours wasted on looking what could be wrong, I noticed that there are new NuGet versions available for System.ServiceModel.* packages. Mine (generated by VS) were 4.4.0, latest were 4.7.0. No, it can't be something stupid like that... yes it was.

I updated NuGet packages and all started working as it should!

I hit it from JMeter with 5 threads (and 100 calls per thread) and voila - 5 connections opened, just like it should!

Alt Text

Final words

If you would like to recreate code from repository, you will need:

I wanted to keep this short (failed as usual), so please ask questions if you are not clear on some of the steps.

Discussion (4)

nlyk profile image

It's really helpful. Unfortunately, I have not been able to add custom HTTP Headers to the request so far. Any suggestions;

nikolicbojan profile image
Bojan Nikolić Author

You can add them through additional DelegatingHandler which you would add to the HttpClient setup. So, create something like TraceLogHandler and append a header.
So, something like

internal class HeaderHandler : DelegatingHandler
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        request.Headers.Add("X-MyHeader", "MyValue");

        return base.SendAsync(request, cancellationToken);
Enter fullscreen mode Exit fullscreen mode

Then in Startup.cs first register it

Enter fullscreen mode Exit fullscreen mode

and then add it to the HttpClient setup e.g. before AddHeaderPropagation line

Enter fullscreen mode Exit fullscreen mode

Let me know if this is not what you had in mind or it doesn't work for your case.

vekzdran profile image
Vedran Mandić

Bojan, thank you so much... this works like a charm! Excellent find and effort! :-)

nikolicbojan profile image
Bojan Nikolić Author

No problem, really glad to hear this post helped! :)