Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dependency Injection for Azure chat bot middleware?

I am working on a new chat bot using Azure Bot Service and QnAMaker. We are using BotBuilder middleware, including custom middleware, to tailor the bot behavior.

One of the middlewares will be calling an Azure function and I would like to use the new HttpClientFactory feature with the custom middleware - but this requires dependency injection.

How can I use dependency injection in BotBuilder middleware like you do with regular .NET Core middleware?

When you look at the bot configuration in the Startup.cs, you can see how it requires you to new up all of the bot dependencies:

services.AddHttpClient<MyFunctionClient>(client =>
{
    client.BaseAddress = new Uri(mySettings.GetValue<string>("myFunctionUrl"));
    client.DefaultRequestHeaders.Add("x-functions-key", mySettings.GetValue<string>("myFunctionKey"));
});

services.AddBot<QnAMakerBot>(options =>
{  
    options.CredentialProvider = new ConfigurationCredentialProvider(Configuration);

    options.ConnectorClientRetryPolicy = new RetryPolicy(
        new BotFrameworkHttpStatusCodeErrorDetectionStrategy(),
        3,
        TimeSpan.FromSeconds(2), 
        TimeSpan.FromSeconds(20),
        TimeSpan.FromSeconds(1));

    var middleware = options.Middleware;
    middleware.Add(new ConversationState<ChatLog>(new MemoryStorage()));
    middleware.Add(new MyCustomMiddleware()); // <- I want to inject a typed HttpClient here

//... etc. ....

Is there a different way to configure the bot that allows for dependency injection?

If MyCustomMiddleware requires a typed HttpClient in its constructor, I have to create a new instance right here, so I don't get the benefit of the DI and the configuration I just set up.

like image 904
emaia Avatar asked May 21 '18 00:05

emaia


1 Answers

While I am not a fan of service locator pattern, the current design of the bot configuration is not very dependency injection friendly.

Using the nature of how the bot middleware are setup but having to provide a new instance during startup, I came up with the following work around.

public class BotMiddlewareAdapter<TMiddleware> : IMiddleware
    where TMiddleware : IMiddleware {
    private readonly Lazy<TMiddleware> middleware;

    public BotMiddlewareAdapter(IServiceCollection services) {
        middleware = new Lazy<TMiddleware>(() =>
            services.BuildServiceProvider().GetRequiredService<TMiddleware>());
    }

    public Task OnTurn(ITurnContext context, MiddlewareSet.NextDelegate next) {
        return middleware.Value.OnTurn(context, next);
    }
}

It takes the IServiceCollection as an explicit dependency and defers the creation of the service provider and eventual resolution of the actual middleware in a factory delegate.

It can then be implemented using

middleware.Add(new BotMiddlewareAdapter<MyCustomMiddleware>(services));

When the adapter is invoked it will lazy resolve the intended middleware on initial call and then invoke it.

In fact you can take this another step further and convert it to an extension method

public static class BotBuilderMiddlewareExtension {
    public static void Add<TMiddleware>(this IList<IMiddleware> middleware, IServiceCollection services) 
        where TMiddleware : IMiddleware {
        middleware.Add(new BotMiddlewareAdapter<TMiddleware>(services));
    }
}

Which simplifies the setup to

middleware.Add<MyCustomMiddleware>(services);
like image 76
Nkosi Avatar answered Sep 28 '22 11:09

Nkosi