I am trying to understand how to properly use the TwilioRestClient
with Dependency Injection. I read this page announcing the AddTwilioClient service and did some experiments.
The goal is to access multiple Twilio subaccounts.
Program.cs:
///Removed code for clarity
var host = new HostBuilder()
.ConfigureServices(services =>
{
services.AddHttpClient();
services.AddTwilioClient();
services.AddScoped<TwilioVoiceServices>();
}).Build();
Here is an injected class that shows the two options for creating the TwilioClient
TwilioVoiceService.cs:
public class TwilioVoiceServices
{
///Removed code for clarity
private readonly ITwilioRestClient twilioClient
public TwilioVoiceServices(ITwilioRestClient twilioClient)
{
_twilioClient = twilioClient;
}
public async Task<CallResource> GetCallWithNamedHttpClient(string accountSid, string authToken, string callSid)
{
var call = await CallResource.FetchAsync(
pathAccountSid: accountSid,
pathSid: callSid,
client: new TwilioRestClient(username: accountSid, password: authToken, httpClient: _twilioClient.HttpClient);
return call;
}
public async Task<CallResource> GetCallWithImpliedHttpClient(string accountSid, string authToken, string callSid)
{
var call = await CallResource.FetchAsync(
pathAccountSid: accountSid,
pathSid: callSid,
client: new TwilioRestClient(username: accountSid, password: authToken));
return call;
}
}
Both GetCallWithNamedHttpClient
and GetCallWithImpliedHttpClient
work and allow me to access the Twilio REST API using a dynamically named Sid/AuthToken.
Do I need to specify the httpClient: as the injected ITwilioRestClient
as shown in GetCallWithNamedHttpClient
? Or, does the Twilio library take care of that automatically as shown in GetCallWithImpliedHttpClient
?
Since both work, my concern is that the GetCallWithImpliedHttpClient
is actually creating new HttpClient
s behind the scenes and not reusing the singleton HttpClient
that services.AddTwilioClient()
creates.
This is deployed to an Azure Function on .NET 7.
When using .AddTwilioClient()
from the Twilio.AspNet.Core
library, the Twilio REST client will use the IHttpClientFactory.CreateClient("Twilio")
to get the HttpClient
. (Source code)
So in GetCallWithNamedHttpClient
you will be reusing that same HttpClient
managed by the HTTP client factory, while in GetCallWithImpliedHttpClient
it will create a new HttpClient
every time. (Source code)
Reusing the HTTP client provided by the HTTP client factory would be better, so I'd go with GetCallWithNamedHttpClient
, however, instead of injecting the ITwilioRestClient
, I'd request an HTTP client from the HTTP client factory and pass that into the constructor of TwilioRestClient
. Like this:
public class TwilioVoiceServices
{
private readonly IHttpClientFactory _clientFactory;
public TwilioVoiceServices(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task<CallResource> GetCallWithNamedHttpClient(string accountSid, string authToken, string callSid)
{
var httpClient = _clientFactory.CreateClient();
var call = await CallResource.FetchAsync(
pathAccountSid: accountSid,
pathSid: callSid,
client: new TwilioRestClient(
username: accountSid,
password: authToken,
httpClient: new SystemNetHttpClient(httpClient)
)
);
return call;
}
}
Now you don't really need to call .AddTwilioClient
during startup since you're not injecting ITwilioRestClient
anymore.
In this case, I think it makes even more sense to create a factory class for the Twilio client:
public class TwilioClientFactory
{
private readonly IHttpClientFactory _clientFactory;
public TwilioClientFactory(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public ITwilioRestClient CreateClient(string accountSid, string authToken)
{
var httpClient = _clientFactory.CreateClient();
return new TwilioRestClient(
username: accountSid,
password: authToken,
httpClient: new SystemNetHttpClient(httpClient)
);
}
}
Register the factory as a singleton and inject it anywhere you need it, call CreateClient
and pass it into the resource methods:
var twilioClient = twilioClientFactory.CreateClient(
accountSid: "",
authToken: ""
);
var call = await CallResource.FetchAsync(
pathSid: "",
client: twilioClient
);
Note that I'm not specifying the accountSid
since it'll use the twilioClient.AccountSid
.
If you only have a couple of accounts, it may be useful to subclass the Twilio client and add those subclasses to the DI container, like this:
using Twilio.AspNet.Core;
using Twilio.Clients;
using Twilio.Http;
var host = new HostBuilder()
.ConfigureServices((context, services) =>
{
services.AddHttpClient();
var config = context.Configuration;
var ivrClientOptions = config.GetSection("Twilio:Client:Ivr").Get<TwilioClientOptions>();
services.AddScoped<IvrTwilioClient>(provider => new IvrTwilioClient(
username: ivrClientOptions.AccountSid,
password: ivrClientOptions.AuthToken,
httpClient: new SystemNetHttpClient(provider.GetRequiredService<IHttpClientFactory>().CreateClient())
));
var sipClientOptions = config.GetSection("Twilio:Client:Sip").Get<TwilioClientOptions>();
services.AddScoped<SipTwilioClient>(provider => new SipTwilioClient(
username: sipClientOptions.AccountSid,
password: sipClientOptions.AuthToken,
httpClient: new SystemNetHttpClient(provider.GetRequiredService<IHttpClientFactory>().CreateClient())
));
}).Build();
public class IvrTwilioClient : TwilioRestClient
{
public IvrTwilioClient(
string username,
string password,
string accountSid = null,
string region = null,
Twilio.Http.HttpClient httpClient = null,
string edge = null
)
: base(username, password, accountSid, region, httpClient, edge)
{
}
}
public class SipTwilioClient : TwilioRestClient
{
public SipTwilioClient(
string username,
string password,
string accountSid = null,
string region = null,
Twilio.Http.HttpClient httpClient = null,
string edge = null
)
: base(username, password, accountSid, region, httpClient, edge)
{
}
}
Now you can have IvrTwilioClient
and SipTwilioClient
injected anywhere.
You can also use a combination of the above approaches.