RicoSuter / NSwag

The Swagger/OpenAPI toolchain for .NET, ASP.NET Core and TypeScript.
http://NSwag.org
MIT License
6.76k stars 1.29k forks source link

Change generated clients to be noob-safe in how they use HttpClient #1097

Open ishepherd opened 6 years ago

ishepherd commented 6 years ago

Edit So I had some noob misunderstanding in HttpClient best practice.

I appreciate you have added stuff for control of HttpClient lifecycle, but perhaps you can make this safe out of the box? Noobs like me might also learn something by reviewing the generated code.

Here is one approach: https://github.com/dotnet/corefx/issues/11224#issuecomment-347361456


Original comments...

According to https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

After each call, even though you've disposed the HttpClient, Windows holds the socket(s) it was using open for 240 seconds. There are only thousands of sockets available, so you can achieve socket exhaustion quite easily.

Although [HttpClient] implements the IDisposable interface it is actually a shared object. This means that under the covers it is reentrant) and thread safe. Instead of creating a new instance of HttpClient for each execution you should share a single instance of HttpClient for the entire lifetime of the application. ...

  1. Make your HttpClient static.
  2. Do not dispose of or wrap your HttpClient in a using unless you explicitly are looking for a particular behaviour (such as causing your services to fail).
ishepherd commented 6 years ago

Reading further...

Although some MSDN agrees...

HttpClient is intended to be instantiated once and reused throughout the life of an application. The following conditions can result in SocketException errors:

  • Creating a new HttpClient instance per request.
  • Server under heavy load.

Creating a new HttpClient instance per request can exhaust the available sockets.

...it can create another issue: You don't want to keep a socket open forever, for example if there is a DNS flip to a different deployment, you will still be talking to the old one.

Some people have custom logic so that both issues are solved. http://disq.us/p/1d4okj9 https://github.com/dotnet/corefx/issues/11224#issuecomment-347361456

RicoSuter commented 6 years ago

My problem with HttpClient is that there is no single or commonly used best practice... This is why I it is currently up to the user to correctly use it :-). The question: What is the best approach to solve this problem?

As you may know there are currently three ways to handle HttpClient in the generated code:

Maybe we should implement a separate library which provides which provides HttpClient proxy implementations which internally manage HttpClient based on time or whatever - the library could provide different implementation which can be chosen depending on the use case/scenario?

ishepherd commented 6 years ago

I'll bet the commonly used 'best' practice is

  • ~Create an HttpClient per request (no injection, default)~

Devs think "I Disposed it, I did the right thing!", not realizing that creating too many HttpClients within 240 seconds will break their system...

My conclusion is that HttpClients should always be static (per my MSDN link) but, because this might lead to it reusing a single socket forever, kept in a pool with round-robin and expiry time.

The pool would need a couple of parameters

I think this would work for everyone?

Obviously I will be looking for a prebuilt pool class... will have a dig around

Apparently this is faster as well as safer: https://github.com/mspnp/performance-optimization/blob/master/ImproperInstantiation/docs/LoadTesting.md

Edit collection of info on this issue https://softwareengineering.stackexchange.com/a/330379/199040

RicoSuter commented 6 years ago

So is there a widely used NuGet package with a good HttpClient wrapper which we could use or propose to use?

ishepherd commented 6 years ago

@RSuter I haven't had time to look into this any further. Hopefully next month.

bulfonjf commented 6 years ago

How about this wrapper? https://github.com/NimaAra/Easy.Common/blob/master/Easy.Common/RestClient.cs

ishepherd commented 6 years ago

You could code a basic version directly in the generator to avoid sprouting another dependency. What we're now doing is

private static readonly ObjectWithLifespan<HttpClient> OurHttpClient = new ObjectWithLifespan<HttpClient>(
    lifespan: TimeSpan.FromSeconds(30),
    creator: () => new HttpClient());

public class ObjectWithLifespan<T>
{
    private class Box
    {
        public T Instance;
        public DateTime Expiry;
    }

    private Box Instance;
    private readonly TimeSpan Lifespan;
    private readonly Func<T> Creator;

    /// <summary>
    /// Function provided must be safe. It might be invoked multiple times needlessly or concurrently,
    /// the value you get may not be from the latest invocation
    /// </summary>
    public ObjectWithLifespan(TimeSpan lifespan, Func<T> creator)
    {
        Lifespan = lifespan;
        Creator = creator ?? throw new ArgumentNullException(nameof(creator));
    }

    public T Value
    {
        get
        {
            var current = Instance;
            if (current == null || current.Expiry <= DateTime.UtcNow)
            {
                current = new Box { Instance = Creator(), Expiry = DateTime.UtcNow + Lifespan };
                Instance = current;
            }
            return current.Instance;
        }
    }

}
RicoSuter commented 6 years ago

I think we should provide an implementation for IHttpClient in a NuGet package so that you can specify IHttpClient instead of HttpClient in the NSwag settings and use this NuGet package with the special implementation...

ishepherd commented 6 years ago

Whatever approach, I think it should be the default. I don't see that all the confusion around this is really justified, it oughta be completely possible to have a 'right' solution

@bulfonjf How about this wrapper? https://github.com/NimaAra/Easy.Common/blob/master/Easy.Common/RestClient.cs

depends on ServicePointManager - looks like it's problematic on .NET Core... https://github.com/dotnet/corefx/issues/11224

cremor commented 6 years ago

Microsoft will provide a HttpClientFactory in ASP.NET Core 2.1. See https://github.com/aspnet/HttpClientFactory for the code and https://blogs.msdn.microsoft.com/webdev/2018/02/02/asp-net-core-2-1-roadmap/#httpclientfactory for some documentation.

reberinformatik commented 6 years ago

Probably the implementation of Flurl could help. It uses HttpClient under the hood and a factory to instantiate it.

Eneuman commented 5 years ago

Now that we have services.AddHttpClient in .Net Core 2.1+, I would suggest changing the C# client template in the following way:

The public constructor:

        public ProcessmotorClient(System.Net.Http.HttpClient httpClient)
        {
            BaseUrl = baseUrl; 
            _httpClient = httpClient; 
            _settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(() => 
            {
                var settings = new Newtonsoft.Json.JsonSerializerSettings();
                UpdateJsonSerializerSettings(settings);
                return settings;
            });
        }

is changed to

        public ProcessmotorClient(string baseUrl, System.Net.Http.HttpClient httpClient)
        {
            _httpClient = httpClient; 
            _settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(() => 
            {
                var settings = new Newtonsoft.Json.JsonSerializerSettings();
                UpdateJsonSerializerSettings(settings);
                return settings;
            });
        }

BaseUrl is removed in favor for httpClient.BaseAddress

This change will make it possible to do this (in startup.cs) without adding any extra code:

    services.AddHttpClient<MyClient>(client =>
    {
        client.BaseAddress = new Uri("https://api.myclient.com/");
    });

Using AddHttpClient this way also solves problems with port starvation and DNS lookup issues in Micro Services.

RicoSuter commented 5 years ago

Have you tried to disable the UseBaseUrl setting?

image

Produces:

image

Eneuman commented 5 years ago

Nice :) I didn't know this already existed. Can this also be set as a parameter in .projcs when using the preview of NSwag.MSBuild.CodeGeneration? (I'm trying to automaticly create a Nuget Package of the client each time I check in code to Azure DevOps)

RicoSuter commented 5 years ago

I think with the NSwagOptions attribute you can pass /UseBaseUrl:false - but not sure if the attribute is called like this...

Eneuman commented 5 years ago

It worked great! Thanks! I added the following parameter: GenerateNSwagCSharpOptions="/UseBaseUrl:false"

RicoSuter commented 5 years ago

BTW: NSwag.MSBuild.CodeGeneration has been unlisted/renamed to NSwag.ApiDescription.Design

stephengtuggy commented 3 years ago

In .NET Core 2.1 and up, I believe the preferred pattern is to use IHttpClientFactory.CreateClient() every time you need an HttpClient instance. The default factory implementation then creates HttpClient instances that clean up their underlying sockets and other resources after 2 minutes.

What about turning _httpClient into a read-only, private property? Then in the property get, on .NET Core 2.1+ you could call IHttpClientFactory.CreateClient(), while on .NET Framework you could just use a static HttpClient instance, or else a custom algorithm that imitates the IHttpClientFactory behaviour. Though the initialization of generated clients might be complicated if you supported all of the above.

Thoughts?

SebastianStehle commented 2 years ago

I think it would make more sense to have a custom IHttpClientProvider interface. Then we can implement this in a backwards compatible way with a `StaticHttpClientProvider" implementation that is used under the hood.

stephengtuggy commented 2 years ago

I think it would make more sense to have a custom IHttpClientProvider interface. Then we can implement this in a backwards compatible way with a `StaticHttpClientProvider" implementation that is used under the hood.

Yes. This is what I am now leaning towards. I have implemented this myself in a project or two.

stephengtuggy commented 2 years ago

And maybe give your IHttpClientProvider interface a read-only, boolean property that indicates whether or not user code should Dispose() of the IHttpClient it got after it's done with it. ShouldDisposeAfterUse or something like that.

SebastianStehle commented 2 years ago

Or a release method. It is a typical pattern for object pooling. See ObjectPool class