I would like to override the Bot Builder's Autofac configuration so that I can provide my own custom ConnectorClient sub class, instead of the default one.
I want to do this so that I can pass a static HttpClient to the ConnectorClient. We're getting socket exhaustion problems in the cloud, and I think HttpClient reuse would help a lot. _This ticket is just a question about overriding ConnectorClient in Autofac though, not the socket exhaustion issue (that's logged as another ticket)_
/* Global.asax.cs configuration to inject my custom autofac module */
protected async void Application_Start()
{
var containerBuilder = new ContainerBuilder();
GlobalConfiguration.Configure(WebApiConfig.Register);
MicrosoftAppCredentials.TrustServiceUrl(@"https://facebook.botframework.com", DateTime.MaxValue);
MicrosoftAppCredentials.TrustServiceUrl(@"https://directline.botframework.com", DateTime.MaxValue);
MicrosoftAppCredentials.TrustServiceUrl(@"https://bc-directline-weurope.azurewebsites.net", DateTime.MaxValue);
ConfigureDependencyInjection(containerBuilder);
}
private static void ConfigureDependencyInjection(ContainerBuilder containerBuilder)
{
// register the Bot Builder module
containerBuilder.RegisterModule(new DialogModule());
// Override bot framework defaults.
containerBuilder.RegisterModule(new MyOwnDiModule());
var config = GlobalConfiguration.Configuration;
// Register your Web API controllers.
containerBuilder.RegisterApiControllers(Assembly.GetExecutingAssembly());
containerBuilder.RegisterWebApiFilterProvider(config);
// Set the dependency resolver to be Autofac.
var container = containerBuilder.Build();
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
}
public class MyOwnDiModule: Module
{
static HttpClient _globalHttpClient = new HttpClient();
protected override void Load(ContainerBuilder builder)
{
base.Load(builder);
//ConnectorClient override that takes a global static HttpClientHandler.
builder
.Register(c => new MyCustomConnectorClient(_globalHttpClient, new Uri(c.Resolve<IAddress>().ServiceUrl), c.Resolve<MicrosoftAppCredentials>(), _globalHttpClientHandler))
.As<IConnectorClient>()
.InstancePerMatchingLifetimeScope(typeof(DialogModule));
}
}
I have not created the MyCustomConnectorClient yet, but setting a breakpoint on the Autofac IConnectorClient registration shows that my custom Register() delegate is never called.
Nothing fancy here. This is my root dialog, and when context.PostAsync is called, I would like MyCustomConnectorClient to be used internally.
[Serializable]
public class MyRootDialog: IDialog<string>
{
public async Task StartAsync(IDialogContext context)
{
await context.PostAsync($"Testing dialog started. Hit me.");
context.Wait(ResumeHere);
}
private async Task ResumeHere(IDialogContext context, IAwaitable<IMessageActivity> result)
{
await context.PostAsync($"Msg received thanks. time:{DateTime.Now.ToLongTimeString()}");
}
}
The messageController does not do anything special, except use Conversation to launch the root dialog:
/// <summary>
/// POST: api/Messages
/// </summary>
public async Task<HttpResponseMessage> Post([FromBody] Activity activity)
{
if (activity.Type == ActivityTypes.Message)
{
await Conversation.SendAsync(activity, () => new MyRootDialog());
}
return Request.CreateResponse(HttpStatusCode.OK);
}
How do I supply a custom sub class of ConnectorClient? (or alternatively, how do I use a static instance of HttpClient?)
Maybe what you should try to register is a custom implementation of the IConnectorClientFactory instead of IConnectorClient.
If you look into ConnectorClientFactory which implements IConnectorClientFactory and is registered as the default implementation within Autofac, you'll notice this:
IConnectorClient IConnectorClientFactory.MakeConnectorClient()
{
return new ConnectorClient(this.serviceUri, this.credentials, true, Array.Empty<DelegatingHandler>());
}
Thanks @alexandruf , I did actually make some progress yesterday and this was exactly it. Not 100% there yet, as even with my custom connectorClient I still struggle with sharing a static HttpClient (it's getting disposed somewhere after the first call).
I'll share some details later.
Here is what we did eventually, and it had a big positive impact on socket usage. Worth creating a PR for?
Here we tell the bot framework to use our own custom stateClient and connectorClient (via our BotFrameworkOverrideDefaultsModule )
protected async void Application_Start()
{
var containerBuilder = new ContainerBuilder();
GlobalConfiguration.Configure(WebApiConfig.Register);
MicrosoftAppCredentials.TrustServiceUrl(@"https://facebook.botframework.com", DateTime.MaxValue);
MicrosoftAppCredentials.TrustServiceUrl(@"https://directline.botframework.com", DateTime.MaxValue);
MicrosoftAppCredentials.TrustServiceUrl(@"https://bc-directline-weurope.azurewebsites.net", DateTime.MaxValue);
var botFrameworkOverrideDefaultsModule = new BotFrameworkOverrideDefaultsModule();
containerBuilder.RegisterModule(botFrameworkOverrideDefaultsModule);
//To ensure that when we call the bot framework's Conversation.SendAsync(), that our custom module is registered with autofac.
// Conversation.cs's static constructor does its own "registerModule" bit, so this is necessary; Otherwise
// the bot framework does not use our overrides.
Conversation.UpdateContainer(bld => bld.RegisterModule(botFrameworkOverrideDefaultsModule));
var container = containerBuilder.Build();
//Hook up the DIContainer to ASP.NET WebApi
var config = GlobalConfiguration.Configuration;
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
}
public class BotFrameworkOverrideDefaultsModule: Module
{
protected override void Load (ContainerBuilder builder)
{
base.Load(builder);
if (ConfigurationManager.AppSettings["BotFrameworkUseStaticHttpClient"] == "true")
{
//ConnectorClient override that takes a global static HttpClient
builder
.Register(c => new HttpClientReuseConnectorClient(new Uri(c.Resolve<IAddress>().ServiceUrl), c.Resolve<MicrosoftAppCredentials>()))
.As<IConnectorClient>()
.InstancePerMatchingLifetimeScope(typeof(DialogModule));
//StateClient override that takes a static HttpClient. I realize this client is being deprecated soon.
builder
.Register(c => new HttpClientReuseStateClient(new Uri(c.Resolve<IAddress>().ServiceUrl), c.Resolve<MicrosoftAppCredentials>()))
.As<IStateClient>()
.InstancePerLifetimeScope();
}
}
}
A static HttpClient is created per uri.
public class HttpClientReuseStateClient: StateClient
{
//List of reusable httpClients, by uri.
public static ConcurrentDictionary<string, HttpClient> StaticHttpClients { get; set; } = new ConcurrentDictionary<string, HttpClient>();
public static HttpClientHandler StaticClientHandler = new HttpClientHandler();
public static HttpClient StaticHttpClient;
public HttpClientReuseStateClient(Uri uri, MicrosoftAppCredentials microsoftAppCredentials)
: base(uri, microsoftAppCredentials)
{
base.HttpClient = StaticHttpClients.GetOrAdd(
uri.ToString(),
u => new HttpClient(
handler: StaticClientHandler,
//To keep the connection open after use.
disposeHandler: false)
{ BaseAddress = new Uri(u.ToString()) });
}
protected override void Dispose(bool disposing)
{
//*Don't call base to dispose; We want to keep the HttpClient connection open otherwise we get
// socket exhaustion errors under load.
//base.Dispose(false);
}
}
public class HttpClientReuseConnectorClient: ConnectorClient
{
//List of reusable httpClients, by uri.
public static ConcurrentDictionary<string, HttpClient> StaticHttpClients { get; set; } = new ConcurrentDictionary<string, HttpClient>();
public static HttpClientHandler StaticClientHandler = new HttpClientHandler();
public static HttpClient StaticHttpClient;
public HttpClientReuseConnectorClient(Uri uri, MicrosoftAppCredentials microsoftAppCredentials)
: base(uri, microsoftAppCredentials)
{
base.HttpClient = GetStaticHttpClient(uri);
}
public HttpClientReuseConnectorClient(Uri uri)
: base(uri)
{
base.HttpClient = GetStaticHttpClient(uri);
}
private static HttpClient GetStaticHttpClient(Uri uri)
{
return StaticHttpClients.GetOrAdd(
uri.ToString(),
u => new HttpClient(
handler: StaticClientHandler,
//To keep the connection open after use.
disposeHandler: false)
{ BaseAddress = new Uri(u.ToString()) });
}
protected override void Dispose(bool disposing)
{
//*Don't call base to dispose; We want to keep the HttpClient connection open otherwise we get
// socket exhaustion errors under load.
//base.Dispose(disposing);
}
}
@willemodendaal Thank you for sharing your solution! I don't think we want to pull this into the v3 SDK, but this is definitely something that will be addressed in v4 (coming soon).
Again, thank you for pointing this out and for coming up with a good solution!
Hey @willemodendaal
There's an alpha release of v4 here: https://github.com/Microsoft/botbuilder-dotnet
And, there's already been some discussion about static http client here: https://github.com/Microsoft/botbuilder-dotnet/issues/106
Most helpful comment
Here is what we did eventually, and it had a big positive impact on socket usage. Worth creating a PR for?
Global.asax.cs
Here we tell the bot framework to use our own custom stateClient and connectorClient (via our
BotFrameworkOverrideDefaultsModule)Our custom Autofac module
Custom StateClient and ConnectorClient sub-classes that don't dispose their HttpClient.
A static HttpClient is created per uri.