Botbuilder-dotnet: Resolve Services in botbuilder Middleware

Created on 24 Aug 2018  Â·  8Comments  Â·  Source: microsoft/botbuilder-dotnet

I'm trying to see if I can resolve a service that I have registered in the asp.net core IoC Container from middleware. More specifically can I inject dependencies in the OnTurn function of the middleware, here's some sample code:

        public async Task OnTurn(ITurnContext context, MiddlewareSet.NextDelegate next)
        {
          try
          {
               await next();
          }
          catch (T exception)
          {
               // I want to inject _eventManager into this middleware, somehow
                _eventManager.Emit(new ExceptionEvent(context, exception));
          }
        }

If its not possible to inject, can I use the turnContext somehow to either resolve it or pass it along..

Thanks!

backlog

Most helpful comment

It seems this is possible with a small addition to the AddBot extension method:

services.AddSingleton(sp =>
{
    // ...
    var botFrameworkAdapter = new BotFrameworkAdapter(…) {…};
    // ...

    foreach (var middleware in sp.GetServices<IMiddleware>())
    {
        botFrameworkAdapter.Use(middleware);
    }

    return botFrameworkAdapter
});

Then prior to calling AddBot it's possible to configure middleware using e.g. services.AddSingleton<IMiddleware, MyMiddleware>(). You can also keep the foreach (var middleware in options.Middleware) to maintain compatibility.

All 8 comments

TL;DR; - there's no way to do this right now.

Longer answer:
Here's the next problem with dependencies as it relates to middleware right now: middleware instances are singletons. Due to the lack of DI support in the framework from the top down, middleware ended up just being single instance: you're the one constructing the instance during startup, adding it to the middleware pipeline, and then that's the instance that's going to be called for every turn. Therefore there's no current support for creating middleware instances on the fly and thus no way to inject scoped dependencies into those instances.

There was some effort towards at least introducing a service locator style API in older versions of the framework via a Services property on ITurnContext which was going to give you access to those dependencies. If you're familiar with ASP.NET's HttpContext::RequestServices it was very similar.

I never loved that it was service locator style, but the framework isn't DI from the top down, so that was as close as it was going to get without a bigger rewrite. The initial implementation that existed only allowed for services to be introduced into the turn manually and I hadn't yet finished plumbing through resolution of non-explicitly added dependencies to the upstream container. Nobody loved this, so it was reverted completely until it could be done it "right". The Services property is now just the TurnState property (more like HttpContext::Items) and even though it retains some service locator style APIs it's really just a property bag of stuff. In fact, those service locator APIs should just be removed at this point IMHO and just turning TurnState into IDictionary<string, object>, but I digress.

Related to #357.

more DI feedback for us to work through

It seems this is possible with a small addition to the AddBot extension method:

services.AddSingleton(sp =>
{
    // ...
    var botFrameworkAdapter = new BotFrameworkAdapter(…) {…};
    // ...

    foreach (var middleware in sp.GetServices<IMiddleware>())
    {
        botFrameworkAdapter.Use(middleware);
    }

    return botFrameworkAdapter
});

Then prior to calling AddBot it's possible to configure middleware using e.g. services.AddSingleton<IMiddleware, MyMiddleware>(). You can also keep the foreach (var middleware in options.Middleware) to maintain compatibility.

In 4.1 we have introduced a new interface IAdapterIntegration this allows the adapter class to be dependency injected when using the integration layer. Just add your adapter factory to the DI. This can be BotFrameworkAdpater setup however you like with whatever middleware you like or it can be your subclass of the BotAdapter class, just make sure you are providing an implementation of this interface.

Alternatively you can just use ASP MVC directly. Depending on your scenario this can make things much simpler. Here is a sample showing how to do that:

https://github.com/Microsoft/BotBuilder-Samples/tree/master/samples/csharp_dotnetcore/30.asp-mvc-bot

To mimic the HttpContext.RequestServices I've wrote this simple middleware and it works with v4.0.

public class BotContainerMidlleware : IMiddleware
    {
        private readonly IServiceScopeFactory _serviceScopeFactory;

        public BotContainerMidlleware(IServiceScopeFactory scopeFactory)
        {
            _serviceScopeFactory = scopeFactory;
        }

        public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default(CancellationToken))
        {
           using (var scope = _serviceScopeFactory.CreateScope())
            {
                turnContext.TurnState.Add<IServiceProvider>(scope.ServiceProvider);
                await next(cancellationToken);
            }

        }
    }

Later if you want to resolve service you could use

turnContext.TurnState.Get<IServiceProvider>().GetRequiredService<IService>();

or create an Extension like that

public static T GetRequiredService<T>(this ITurnContext turnContext) => 
        turnContext.TurnState.Get<IServiceProvider>().GetRequiredService<T>();
turnContext.GetRequiredService<IService>();

@sbiaudet thanks for your solution. But how would you be able to unit test your middleware having that approach, that is containing a static extension method to resolve those instances.

I've seen this problem over and over again. Parts of a framework don't play well with dependency injection and you're left with a lot of ugly workarounds. In the end we should aim to have dependency injection in the OnTurnAsync method, just like you have with the Invoke method in the ASP.Net Core middleware.

@jvanderbiest I’m ok this solution is not fully compliant with unit testing but it’s a workaround. If you check asp.net core source code you could seen lot of use of httpcontext.requestservices. It’s like a service locator pattern. Not idealy but useful

Me too I would like to have dependency injection in the OnTurnAsync but at this time it’s not possible. Middleware is not structured for this.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Savvato picture Savvato  Â·  6Comments

cmayomsft picture cmayomsft  Â·  6Comments

markbeau picture markbeau  Â·  6Comments

drub0y picture drub0y  Â·  5Comments

nrandell picture nrandell  Â·  6Comments