PowerBIClient is a part of Microsoft.PowerBI.Api package. Developers use it to integrate their apps with PowerBI. There are many sample solutions on the Internet that show how to use it in ASP.NET Core apps. The typical approach is to create a new instance of PowerBIClient
and then use it to invoke web requests. Eventually, it is disposed of. Unfortunately, under the hood, it manually creates HttpClient
and therefore may lead to socket exhaustion. In this post, we will discuss two alternative options of using PowerBIClient
in the ASP.NET Core web app.
What is the problem and what are possible solutions?
The current version of Microsoft.PowerBI.Api (4.2.0) does not allow injecting your own HttpClient
into PowerBIClient
. There is no constructor that allows doing this. However, there are some overloads that take HttpMessageHandler
and DelegatingHandler
instances. Furthermore, if you look at PowerBIClient
source code you will find out that it derives from ServiceClient<>
class. Consequently, wherever PowerBIClient
instance is disposed of, its internal HttpClient
gets disposed of too together with all HTTP handlers that are referenced by ServiceClient
base class.
What can we do? From my research there are two options:
- Register
PowerBIClient
as a singleton. - Override internal
HttpClient
with instance created byIHttpClientFactory
.
It used to be quite common to register HttpClient
as a singleton. The downside of this approach was that HttpClient
did not respond to DNS changes. Since ASP.NET Core 2.1 the suggested solution has been to use IHttpClientFactory
which manages an instance of HttpMessageHandler
(and therefore connections lifetime).Together with .NET Core 2.1 new SocketsHttpHandler
was created. It is the main HttpMessageHandler
of every HttpClient
instance. Internally it manages connections and reacts to DNS changes by replacing them with new ones.
Both solutions mentioned above are applicable to PowerBIClient
and will be presented in this article.
PowerBI API token provider
First of all, both option 1 and option 2 needs to have an API token. I'm going to briefly describe how it works in my application. You can skip this paragraph if you already have it working.
There is a class that implements ITokenProvider
interface (PowerBIClient will use it). It acquires tokens from IConfidentialClientApplication
(from Microsoft.Identity
package) which has client id and client secret of service principal used to authenticate to PowerBI API.
public interface IPowerBIApiTokenProvider : ITokenProvider { }
public class PowerBIApiTokenProvider : IPowerBIApiTokenProvider{ private readonly IConfidentialClientApplication _confidentialClientApplication; private readonly IList<string> _powerBiScopes;
public PowerBIApiTokenProvider( IList<string> powerBiScopes, IConfidentialClientApplication confidentialClientApplication) { _powerBiScopes = powerBiScopes; _confidentialClientApplication = confidentialClientApplication; }
public async Task<AuthenticationHeaderValue> GetAuthenticationHeaderAsync CancellationToken cancellationToken) { var authenticationResult = await _confidentialClientApplication .AcquireTokenForClient(_powerBiScopes) .ExecuteAsync(cancellationToken);
return new("Bearer", authenticationResult.AccessToken); }}
It is registered as a singleton because IConfidentialClientApplication
manages a token cache. I hardcoded (and mocked) service principal data. In real life, I would use IOptions
interface to set appropriate values.
services.AddSingleton<IPowerBIApiTokenProvider, PowerBIApiTokenProvider>(provider =>{ var azureTenantSpecificUrl = "https://login.microsoftonline.com/myTenant/";
var clientApp = ConfidentialClientApplicationBuilder .Create("service principal client id") .WithClientSecret("service principal client secret") .WithAuthority(azureTenantSpecificUrl) .Build();
return new(new List<string>() { "https://analysis.windows.net/powerbi/api/.default" }, clientApp);});
Option 1 – Registering PowerBIClient as a singleton
Below you can see how to register PowerBIClient
as a singleton in the default ASP.NET Core dependency container. In order to log requests with ILogger
we need to create LoggingHttpMessageHandler
and LoggingScopeHttpMessageHandler
to append them to HTTP handlers pipeline. After that, you can inject IPowerBIClient
anywhere in your code.
services.AddSingleton<IPowerBIClient, PowerBIClient>(provider =>{ var logger = provider.GetRequiredService<ILogger<PowerBIClient>>(); var tokenProvider = provider.GetRequiredService<IPowerBIApiTokenProvider>(); var loggingHandler = new LoggingHttpMessageHandler(logger); var loggingScopeHandler = new LoggingScopeHttpMessageHandler(logger);
var httpHandler = new HttpClientHandler();
var client = new PowerBIClient( new TokenCredentials(tokenProvider), httpHandler, loggingHandler, loggingScopeHandler);
return client;});
Under the hood, HttpClientHandler
creates an instance of SocketsHttpHandler
. Unfortunately, HttpClientHandler
stores it in a private field, and as a result, it cannot be accessed. There are some properties in SocketsHttpHandler
that may be interesting for us.
One of them is PooledConnectionLifetime
which defines the maximum lifetime of connection in a pool. By default, it does not have any limit.
Another property is PooledConnectionIdleTimeout
. It describes how long it takes to remove the connection from the pool if it is unused. The default value of this is 2 minutes. Consequently, with default settings, if the given connection is used often (more than once per 2 minutes) it will never be recreated. In some scenarios, this may lead to problems with DNS.
In order to fix that we need to use reflection. First of all, let’s create an extension method on HttpClientHandler
class that will return a current reference to SocketsHttpHandler
.
public static class HttpClientHandlerExtensions{ public static SocketsHttpHandler GetUnderlayingSocketsHttpHandler( this HttpClientHandler handler) { var field = typeof(HttpClientHandler) .GetFields(BindingFlags.NonPublic | BindingFlags.Instance) .SingleOrDefault(x => x.FieldType == typeof(SocketsHttpHandler));
return field?.GetValue(handler) as SocketsHttpHandler; }}
Then we can modify PowerBIClient
factory method. We additionally log an error if for some reason we are not able to get SocketsHttpHandler
. It may happen if the internal implementation of HttpClientHandler
will change. We set PooledConnectionLifetime to 60 minutes so after this timespan connection will be replaced unless there is a request in progress.
services.AddSingleton<IPowerBIClient, PowerBIClient>(provider =>{ var logger = provider.GetRequiredService<ILogger<PowerBIClient>>(); var tokenProvider = provider.GetRequiredService<IPowerBIApiTokenProvider>(); var loggingHandler = new LoggingHttpMessageHandler(logger); var loggingScopeHandler = new LoggingScopeHttpMessageHandler(logger);
var httpHandler = new HttpClientHandler();
var socketHandler = httpHandler.GetUnderlayingSocketsHttpHandler();
if (socketHandler == null) { logger.LogError( "SocketsHandler for PowerBIClient is null and has not been configured."); } else { socketHandler.PooledConnectionLifetime = TimeSpan.FromMinutes(60); }
var client = new PowerBIClient( new TokenCredentials(tokenProvider), httpHandler, loggingHandler, loggingScopeHandler);
return client;});
Option 2 – Overriding internal HttpClient
This solution is quite tricky. We need to create a class that derives from PowerBIClient
. Thanks to that, we have access to HttpClient
which has a protected setter. Then in the constructor, we are able to set our own HttpClient
. In our case, this is created by IHttpClientFactory
. Unfortunately, we invoke the base constructor first – this is how C# works. It creates HttpClient
instance and HttpClientHandler
(plus few other delegating handlers). Thus, we need to dispose of them and delete every reference to them. Thanks to that, the garbage collector will get rid of it. Because PowerBIClientWrapper
will not have a chance to use these handlers, the connections will not be created in the pool.
public class PowerBIClientWrapper : PowerBIClient{ public PowerBIClientWrapper( ServiceClientCredentials credentials, HttpClient httpClient) : base(credentials) { HttpClient?.Dispose(); FirstMessageHandler?.Dispose(); HttpClientHandler?.Dispose();
FirstMessageHandler = null; HttpClientHandler = null;
HttpClient = httpClient; }}
After that, we need to register HttpClient
that will be used by PowerBIClient
internally. By default, PowerBIClient
uses RetryAfterDelegatingHandler
and RetryDelegatingHandler
as delegating handlers. The request may fail if there are too many concurrent requests to PowerBI API at a given moment. In this solution, you can define your own retry policy. For simplicity let’s try to replicate default behavior.
services.AddHttpClient(nameof(PowerBIClientWrapper)) .ConfigurePrimaryHttpMessageHandler(() => { var delegatingHandler = new RetryAfterDelegatingHandler { InnerHandler = new HttpClientHandler() };
return new RetryDelegatingHandler(delegatingHandler); });
Then we can register PowerBIClientWrapper
as scoped so a new instance will be created for every request. It’s ok for us since now we create HttpClient
with IHttpClientFactory
. Because PowerBIClientWrapper
implements the IPowerBIClient
interface, it can be registered under it. Logging will work out of the box.
services.AddScoped<IPowerBIClient>(provider =>{ var tokenProvider = provider.GetRequiredService<IPowerBIApiTokenProvider>(); var httpClientFactory = provider.GetRequiredService<IHttpClientFactory>(); var httpClient = httpClientFactory.CreateClient(nameof(PowerBIClientWrapper)); var client = new PowerBIClientWrapper(new TokenCredentials(tokenProvider), httpClient); return client;});
Although IPowerBIClient
and created HttpClient
are disposed of after each request the underlaying HTTP handlers are not. That’s because handlers used by HttpClient
are not referenced by PowerBIClientWrapper
. Furthermore, during HttpClient
disposal, these handlers are not touched.
Conclusion
Both options have their drawbacks. Registering PowerBIClient
as a singleton requires reflection to configure SocketsHttpHandler
. However, it’s a slightly better solution than disposing of already existing HttpClient
to replace it with the new one.
Both option one and two resolves the socket exhaustion problem. It’s up to you which one you choose. That would be great if there were a constructor that takes HttpClient
in PowerBIClient
. I hope that Microsoft will add that eventually.