Search code examples
c#.net-coredependency-injectionwebapi

Why socket handles are leaked


I have a .Net Core web app deployed to Azure App Service. The API has only one endpoint, and all it does is call another API (inside MyCLass) and return the response.

I have the following in my Startup:

services.AddScoped<IOAuthService, OAuthService>();
services.AddHttpClient<IMyClass, MyClass>(client =>
{
    var authConfig = config.Get<OAuthConfig>();
    var oAuthService = services.BuildServiceProvider().GetRequiredService<IOAuthService>();

    var token = oAuthService.GetTokenAsync(authConfig).GetAwaiter().GetResult();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
});

And here is the OAuthService.GetTokenAsync method:

private IConfidentialClientApplication app;
public async Task<string> GetTokenAsync(OAuthConfig config)
{
    app ??= ConfidentialClientApplicationBuilder.Create(config.ClientId)
        .WithTenantId(config.TenantId)
        .WithClientSecret(config.ClientSecret)
        .WithLegacyCacheCompatibility(false) // No need to share with ADAL.NET; increases performance
        .Build();

    var scopes = new string[] { $"{config.Resource}/.default" };
    var authResult = await app.AcquireTokenForClient(scopes)
        .ExecuteAsync()
        .ConfigureAwait(false);

    return authResult.AccessToken;
}

A HttpClient is injected into MyClass where it calls another API and returns the response. The above code results in socket handle leaks and SNAT port exhaustion.

However, the following changes would solve the problem: 1. Instead of getting the token once in DI, get the token in MyCLass on the fly every time a request is made (without any changes to OAuthService). 2. Changing the above DI code to the following:

services.AddSingleton<IOAuthService, OAuthService>();
services.AddHttpClient<IHdcApiDataConnector, HdcApiDataConnector>();

My question is why exactly the second code solves the socket handle leak problem?


Solution

  • Answering my question.

    Ok after some investigation, I found out what was happening:

    1. Using ??== was an attempt to make sure ConfidentialClientApplicationBuilder is created only once. However, registering IOAuthService as scoped means a new instance is created for every incoming request and hence one ConfidentialClientApplicationBuilder is created in each instance. Making it singleton in the second approach would solve this issue.
    2. services.AddHttpClient<IMyClass, MyClass> registers IMyClass as transient. This means a new instance of MyClass is created for each incoming request. Further, for every such instance, the code services.BuildServiceProvider().GetRequiredService<IOAuthService>() creates another instance of IOAuthService causing the problem mentioned above.

    Aside from the DI issue, there are some other issues with my code above:

    1. As @JoelCoehoorn mentioned in the comments, there is another big flaw in the first approach: tokens expire and should be refreshed.
    2. app ??= ConfidentialClientApplicationBuilder.Create should probably be moved to either the constructor or even in DI, it's very confusing and error-prone to have that kind of logic (making sure the ConfidentialClientApplicationBuilder is singleton) inside the class method.

    Further, note that if the ConfidentialClientApplicationBuilder is singleton, then the tokens are cached under the hood and you don't have to implement refreshing tokens yourself (see this).