3

This article shows a well-known problem with HttpClient that can lead to socket exhaustion.

I have an ASP.NET Core 3.1 web application. In a .NET Standard 2.0 class library I've added a WCF web service reference in Visual Studio 2019 following this instructions.

In a service I'm using the WCF client the way it's described in the documentation. Creating an instance of the WCF client and then closing the client for every request.

public class TestService
{
    public async Task<int> Add(int a, int b)
    {
        CalculatorSoapClient client = new CalculatorSoapClient();
        var resultat = await client.AddAsync(a, b);
        //this is a bad way to close the client I should also check
        //if I need to call Abort()
        await client.CloseAsync();
        return resultat;
    }
}

I know it's bad practice to close the client without any checks but for the purpose of this example it does not matter.

When I start the application and make five requests to an action method that uses the WCF client and then take a look at the result from netstat I discover open connections with status TIME_WAIT, much like the problems in the article above about HttpClient.

enter image description here

It looks to me like using the WCF client out-of-the-box like this can lead to socket exhaustion or am I missing something?

The WCF client inherits from ClientBase<TChannel>. Reading this article it looks to me like the WCF client uses HttpClient. If that is the case then I probably shouldn't create a new client for every request, right?

I've found several articles (this and this) talking about using a singleton or reusing the WCF client in some way. Is this the way to go?

###UPDATE

Debugging the appropriate parts of the WCF source code I discovered that a new HttpClient and HttpClientHandler were created each time I created a new WCF client which I do for every request. You can inspect the code here

internal virtual HttpClientHandler GetHttpClientHandler(EndpointAddress to, SecurityTokenContainer clientCertificateToken)
{
    return new HttpClientHandler();
}

This handler is used in to create a new HttpClient in the GetHttpClientAsync method:

httpClient = new HttpClient(handler);

This explains why the WCF client in my case behaves just like a HttpClient that is created and disposed for every request.

Matt Connew writes in an issue in the WCF repo that he has made it possible to inject your own HttpMessage factory into the WCF client. He writes:

I implemented the ability to provide a Func<HttpClientHandler, HttpMessageHandler> to enable modifying or replacing the HttpMessageHandler. You provide a method which takes an HttpClientHandler and returns an HttpMessageHandler.

Using this information I injected my own factory to be able to control the generation of HttpClientHandlers in HttpClient.

I created my own implementation of IEndpointBehavior that injects IHttpMessageHandlerFactory to get a pooled HttpMessageHandler.

public class MyEndpoint : IEndpointBehavior
{
    private readonly IHttpMessageHandlerFactory messageHandlerFactory;

    public MyEndpoint(IHttpMessageHandlerFactory messageHandlerFactory)
    {
        this.messageHandlerFactory = messageHandlerFactory;
    }

    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {
        Func<HttpClientHandler, HttpMessageHandler> myHandlerFactory = (HttpClientHandler clientHandler) =>
        {
            return messageHandlerFactory.CreateHandler();
        };
        bindingParameters.Add(myHandlerFactory);
    }

    <other empty methods needed for implementation of IEndpointBehavior>

}

As you can see in AddBindingParameters I add a very simple factory that returns a pooled HttpMessageHandler.

I add this behavior to my WCF client like this.

public class TestService
{
    private readonly MyEndpoint endpoint;

    public TestService(MyEndpoint endpoint)
    {
        this.endpoint = endpoint;
    }

    public async Task<int> Add(int a, int b)
    {
        CalculatorSoapClient client = new CalculatorSoapClient();
        client.Endpoint.EndpointBehaviors.Add(endpoint);
        var resultat = await client.AddAsync(a, b);
        //this is a bad way to close the client I should also check
        //if I need to call Abort()
        await client.CloseAsync();
        return resultat;
    }
}

Be sure to update any package references to System.ServiceModel.* to at least version 4.5.0 for this to work. If you're using Visual Studio's 'Add service reference' feature, VS will pull in the 4.4.4 versions of these packages (tested with Visual Studio 16.8.4).

When I run the applications with these changes I no longer have an open connection for every request I make.

Brendan Lane
  • 117
  • 2
  • 8
Edminsson
  • 2,426
  • 20
  • 25
  • 1
    Hi, I know it has been some time, but thank you for this one (together with that Github issue), helped me a lot. One thing that I think is very important to mention - check your NuGet versions for `System.ServiceModel.*` packages! I had them by default on 4.4.0 and wasted few hours wondering why this is not working. When I updated to 4.7.0 all worked like a charm :) – Nick43 Jun 22 '20 at 19:32
  • 1
    You're absolutely right @Nick43. I should have added that piece of information. Thank you! – Edminsson Jun 25 '20 at 07:17
  • Hi, I finally got some time to create sample code and write a blog post on this one. Added all the caveats I encountered and hopefully provided some good practices in the sample. If you have time, check it out here https://dev.to/nikolicbojan/make-soap-requests-using-ihttpclientfactory-in-net-core-46hk – Nick43 Aug 12 '20 at 12:18

2 Answers2

0

You should consider disposing your CalculatorSoapClient. Be aware that a simple Dispose() is usually not enough, becaue of the implementation of the ClientBase. Have a look at https://learn.microsoft.com/en-us/dotnet/framework/wcf/samples/use-close-abort-release-wcf-client-resources?redirectedfrom=MSDN, there the problem is explained.

Also consider that the underlying code is managing your connections, sometimes it will keep them alive for later use. Try calling the server a lot of times to see, if there is a new connection for each call, or if the connections are being reused.

The meaning TIME_WAIT is also discussed here:

https://superuser.com/questions/173535/what-are-close-wait-and-time-wait-states

https://serverfault.com/questions/450055/lot-of-fin-wait2-close-wait-last-ack-and-time-wait-in-haproxy

It looks like your client has done everything required to close the connection and is just waiting for the confirmation of the server.

You should not have to use a singleton since the framework is (usually) taking good care of the connections.

flayn
  • 5,272
  • 4
  • 48
  • 69
  • Thank you @flayn. I've tested several different web services and they all display the same problem with open connections even when I dispose of the client in the recommended way. I've updated my question with an explanation of what I think is causing the problem. – Edminsson Feb 10 '20 at 07:45
0

I created an issue in the WCF repository in Github and got some great answers.

According to Matt Connew and Stephen Bonikowsky who are authorities in this area the best solution is to reuse the client or the ChannelFactory.

Bonikowsky writes:

Create a single client and re-use it.

var client = new ImportSoapClient();

And Connew adds:

Another possibility is you could create a channel proxy instance from the underlying channelfactory. You would do this with code similar to this:

public void Init()
{
    _client?.Close();
    _factory?.Close();
    _client = new ImportSoapClient();
    _factory = client.ChannelFactory;
}

public void DoWork()
{
    var proxy = _factory.CreateChannel();
    proxy.MyOperation();
    ((IClientChannel)proxy).Close();
}

According to Connew there is no problem reusing the client in my ASP.NET Core web application with potentially concurrent requests.

Concurrent requests all using the same client is not a problem as long as you explicitly open the channel before any requests are made. If using a channel created from the channel factory, you can do this with ((IClientChannel)proxy).Open();. I believe the generated client also adds an OpenAsync method that you can use.

UPDATE

Since reusing the WCF Client also means reusing the HttpClient instance and that could lead to the known DNS problem I decided to go with my original solution using my own implementation of IEndpointBehavior as described in the question.

Brendan Lane
  • 117
  • 2
  • 8
Edminsson
  • 2,426
  • 20
  • 25