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?

TL;DR;
Yes it is! You can find sample application here on my GitHub repository https://github.com/nikolic-bojan/soap-client 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 https://github.com/dotnet/wcf/issues/3230 that pointed me to both this blog post https://medium.com/trueengineering/realization-of-the-connections-pool-with-wcf-for-net-core-with-usage-of-httpclientfactory-c2cb2676423e and this PR added to Core https://github.com/dotnet/wcf/pull/2534/files 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 https://docs.microsoft.com/en-us/dotnet/core/additional-tools/wcf-web-service-reference-guide

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 https://medium.com/grensesnittet/integrating-with-soap-web-services-in-net-core-adebfad173fb

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.

Latest comments (11)

Collapse
 
tho_shiteng_a8027f739194 profile image
Tho Shi Teng

Does this design able to cater for soap calls that require basic authentication ? i try to modify an existing soap client to enable http client factory, it always hit error :

The HTTP request is unauthorized with client authentication scheme 'Basic'.

Collapse
 
nikolicbojan profile image
Bojan Nikolić

Hi Tho,
Have you checked Caveat #2 or comment with suggestion for adding HttpHandler with Header for, in your case, basic authentication?
Basic authentication is just a Authorization header with Basic, then space, then Base64 of user:pass.

Collapse
 
datacruze profile image
datacruze

Hi Bojan, nice article. How would I add an X509 certificate to use the soap service with this approach? I currently use the WCF bindings to add the X509 certificate

Collapse
 
nikolicbojan profile image
Bojan Nikolić

Hi, I would suggest that you setup certificate in a constructor of a service, similarly how I setup timeouts here github.com/nikolic-bojan/soap-clie...
Maybe this example could help docs.microsoft.com/en-us/dotnet/fr...

Collapse
 
datacruze profile image
datacruze

Thanks, I added the cert but I still see TIME_WAIT when running netstat. Do you think I did not implement httpmessagehandler correctly? I documented my issue at the link below. Please let me know any suggestions
github.com/dotnet/wcf/issues/4838

Thread Thread
 
nikolicbojan profile image
Bojan Nikolić

Hi @datacruze
I would like to understand this better (checked Issue on GH) - you managed to add certificate and to call WCF service; you also conducted load tests and that was also a success; you load tested with 20 threads in parallel.
HttpMessageHandler has a default 2 minute lifetime, so all your connections should be closed after that period of time.
I checked your code on GH Issue and what I can see is that it is quite different then mine where I inject IHttpMessageHandlerFactory. I am also Closing the channel after success and Aborting on any issue or if it is not a success.
Were all of your calls a success? Could you try to close the Channel each time?

Thread Thread
 
ctechid profile image
Ey

Try to call channel.Abort(), then channel.Close(), channel.Dispose(), channel = null.

Collapse
 
nlyk profile image
nlyk

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

Collapse
 
nikolicbojan profile image
Bojan Nikolić

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

services.AddScoped<HeaderHandler>();
Enter fullscreen mode Exit fullscreen mode

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

.AddHttpMessageHandler<HeaderHandler>()
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.

Collapse
 
vekzdran profile image
Vedran Mandić

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

Collapse
 
nikolicbojan profile image
Bojan Nikolić

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