New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dependency Injection #106

Closed
wiltodelta opened this Issue Apr 14, 2016 · 16 comments

Comments

Projects
None yet
5 participants
@wiltodelta
Contributor

wiltodelta commented Apr 14, 2016

How to use Dependency Injection with Dialogs?

@willportnoy

This comment has been minimized.

Member

willportnoy commented Apr 18, 2016

We use Autofac as our DI container:

https://github.com/Microsoft/BotBuilder/blob/master/CSharp/Library/Dialogs/DialogModule.cs

is there something specific I can help with?

@wiltodelta

This comment has been minimized.

Contributor

wiltodelta commented Apr 19, 2016

@willportnoy We also use Autofac in our project :) Need an example of injecting external dependencies in Dialog. Now I register BotService at application start and after using it in BotDialog like this:

[Serializable]
public class BotDialog : IDialog<object>
{
    private static readonly BotService _botService;

    static BotDialog()
    {
        _botService = (BotService) GlobalConfiguration.Configuration
            .DependencyResolver.GetService(typeof (BotService));
    }

Maybe there is a better option?

@willportnoy

This comment has been minimized.

Member

willportnoy commented Apr 19, 2016

Generally, constructor injection is the best path for dependency injection, because then instantiated objects are "completely constructed" after the constructor runs.

It's generally better to avoid the service locator pattern.

Since you instantiate the dialogs (both the root dialog as passed by the factory method to Conversation.SendAsync, and child dialogs are passed to context.Call), you should be able to inject dependencies.

Maybe you can use Autofac's delegate factories?

@wiltodelta

This comment has been minimized.

Contributor

wiltodelta commented Apr 20, 2016

Thanks, I know that constructor injection is best option. But, if I use constructor injection in Dialog, I have problem with serialization of BotService in Dialog.

@wiltodelta

This comment has been minimized.

Contributor

wiltodelta commented Apr 20, 2016

I found a way to use constructor injection in Dialog. I use a static modifier for _botService field that does not participate in serialization of Dialog.

    [Serializable]
    public class BotDialog : IDialog<object>
    {
        private static BotService _botService;

        public BotDialog(BotService botService)
        {
            _botService = botService;
        }

Issue can be closed.

@willportnoy

This comment has been minimized.

Member

willportnoy commented Apr 20, 2016

I understand your question better now. We have a similar issue with service objects that we want to instantiate from the container rather than from the serialized blob. Here is how we register those objects in the container - we apply special handling during deserialiation for all objects with the key Key_DoNotSerialize:

        builder
            .RegisterType<BotToUserQueue>()
            .Keyed<IBotToUser>(FiberModule.Key_DoNotSerialize)
            .AsSelf()
            .As<IBotToUser>()
            .SingleInstance();
@willportnoy

This comment has been minimized.

Member

willportnoy commented Apr 22, 2016

I'm closing this issue for now - feel free to re-open if it hasn't been resolved.

@Unders0n

This comment has been minimized.

Unders0n commented Jun 5, 2017

Sorry guys i didn't get the proper way to inject services to dialogs and also link provided by @willportnoy is 404 now . Also @wiltodelta 's code make no sense since you can't assign static variable.
My case is that i create dialog using Autofac resolving and passing service via ctor, and from this dialog i create new one with

var myform = new FormDialog<WelcomePoll>(answers, WelcomePoll.BuildForm,
                FormOptions.PromptInStart, null);         
            context.Call(myform, CustomerInfoResult);

All resolvings of services went fine before it hits callback CustomerInfoResult, here i can see that my service is null.

Ony way i found to be working is this:

private async Task CustomerInfoResult(IDialogContext context, IAwaitable<WelcomePoll> result)
       {
           using (
               var scope = DialogModule.BeginLifetimeScope(Conversation.Container, context.Activity.AsMessageActivity())
           )
           {
               globalSettingsService = scope.Resolve<GlobalSettingsService>();
           }

...
Thank you

@Unders0n

This comment has been minimized.

Unders0n commented Jun 28, 2017

Sorry, do you have update on this? Also im struggling to find right way to configure lifetime for my DbContext and Repositories. And to inject all of this not only to messageControllers but to regular ones. DialogModule.BeginLifetimeScope not seems to have sense there cos there's no activity and regular autofac webapi way to inject dependencies also have its issues.
I would be very helpful if someone provide good examples on this.

@willportnoy

This comment has been minimized.

Member

willportnoy commented Jul 11, 2017

this is a sample that shows how to resolve services from dialog constructors and the container:

https://github.com/Microsoft/BotBuilder/blob/master/CSharp/Samples/AlarmBot/Models/AlarmModule.cs#L24

@Unders0n

This comment has been minimized.

Unders0n commented Jul 13, 2017

@willportnoy thanks, that was pretty useful, get dialogs and services to work with this aproach.
The only issue now that regular controllers (for example for slack interactive messages) are not working,
cos they trying to resolve dependencies in same manner and throws error that it can't locate lifetimescope (which is logical): Unable to resolve the type 'BusinessLayer.CountryService' because the lifetime scope it belongs in can't be located.
What is the recommended aproach for this case? Shall i configure regular controllers and services for them in other module?

The only workaround i've found is to manually register controller in this way:
builder.Register((c, p) => new InteractiveMenuController(new CountryService(new MyContext()), new LoggerService<ILogger>())).AsSelf().InstancePerDependency(); but it have no sense
Some code:

//register service
                builder.RegisterType<CountryService>().Keyed<ICountryService>(FiberModule.Key_DoNotSerialize).AsImplementedInterfaces().InstancePerMatchingLifetimeScope(DialogModule.LifetimeScopeTag);
//register dbcontext
                builder.RegisterType<MyContext>().Keyed<IDbContext>(FiberModule.Key_DoNotSerialize).AsImplementedInterfaces().InstancePerMatchingLifetimeScope(DialogModule.LifetimeScopeTag);
///register controllers
...
// Register your Web API controllers.
                builder.RegisterApiControllers(Assembly.GetExecutingAssembly());

                // OPTIONAL: Register the Autofac filter provider.
                builder.RegisterWebApiFilterProvider(config);

               /* builder.RegisterType<MessagesController>().InstancePerDependency();
                builder.RegisterType<InteractiveMenuController>().InstancePerDependency();*/

                GlobalConfiguration.Configuration.DependencyResolver =
                    new AutofacWebApiDependencyResolver(Conversation.Container);
@willportnoy

This comment has been minimized.

Member

willportnoy commented Aug 10, 2017

Wouldn't CountryService be registered in some parent container, such that the lifetime scope would be available? Does CountryService have some dependency on something with a more narrow lifetime scope?

@Unders0n

This comment has been minimized.

Unders0n commented Jan 9, 2018

@willportnoy , sorry not sure i totally get what you mean. For now CountryService depends only on Ef dbcontext. What you mean by "registered in some parent container".
Maybe you could provide some links on documentation on usage of autofac in context of using it with ms chatbot, 'cos i can't find any good samples on that. For example i still have problems with using
var builder = new ContainerBuilder(); builder.Build();
instead of
builder.Update(Conversation.Container);
and with latter i can't get Quartz.Net working with autofac DI
Thank you

@Unders0n

This comment has been minimized.

Unders0n commented Jan 9, 2018

Update: actually i managed to fix the issue, i've downloaded last samples of AlarmBot and made everything just like there. Though still would be really cool if you guys provide more documentation on this aspect. Also i've heard that you're planning to move to another IOC container, can you provide more info on that?

@MovGP0

This comment has been minimized.

MovGP0 commented Mar 5, 2018

Unfortunately, Bot Builder SDK was not built with Dependency Injection (DI) in mind. For proper serialization and DI, there must be a clear distinction between State (Entities that gets serialized) and Behaviour (Services that change the state of entities). The problem is that a Dialog is both, which I think is a design flaw, that gives me a headache (for real; no joke).

So this is how to work around this:

  1. Setup your dependencies like this:
    public static class ContainerBuilderExtensions
    {
        public static void RegisterFactory<T>(this ContainerBuilder builder)
        {
            builder.RegisterType<T>().InstancePerDependency();
            builder.Register<Func<T>>(r =>
            {
                var context = r.Resolve<IComponentContext>();
                return () => context.Resolve<T>();
            }).InstancePerDependency();
        }
    }

    public static class DependencyInjection
    {
        public static void Setup(ContainerBuilder builder)
        {
            builder.RegisterModule(new AzureModule(Assembly.GetExecutingAssembly()));
            builder.RegisterApiControllers(typeof(MessagesController).Assembly);

            var configuration = GetConfiguration();
            builder.Register(_ => configuration).As<IConfiguration>().SingleInstance();
            builder.RegisterFactory<RootDialog>();

           // TODO: register further services here...
        }

        private static IConfigurationRoot GetConfiguration()
        {
            return new ConfigurationBuilder()
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .AddEnvironmentVariables()
                .Build();
        }
    }
  1. Register all dependencies in Global.asax like this:
    public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            Conversation.UpdateContainer(DependencyInjection.Setup);
            GlobalConfiguration.Configuration.DependencyResolver = new AutofacWebApiDependencyResolver(Conversation.Container);
            GlobalConfiguration.Configure(WebApiConfig.Register);
        }
    }
  1. Inject the services into the ApiController:
    [BotAuthentication]
    [Route("api/messages")]
    public sealed class MessagesController : ApiController
    {
        public MessagesController(Func<RootDialog> rootDialogFactory)
        {
            RootDialogFactory = rootDialogFactory ?? throw new ArgumentNullException(nameof(rootDialogFactory));
        }
  1. Inject the services into the Dialog and implement ISerializable manually to prevent serialization of the services:
    [Serializable]
    public sealed class RootDialog : IDialog<object>, ISerializable
    {
        public RootDialog(IBingSpellCheckService bingSpellCheckService)
        {
            BingSpellCheckService = bingSpellCheckService ?? throw new ArgumentNullException(nameof(bingSpellCheckService));
        }

        private IBingSpellCheckService BingSpellCheckService { get; }
        
        #region ISerializable
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            // TODO: implement serialization
        }

        private RootDialog(SerializationInfo info, StreamingContext context)
        {
            if (info == null) throw new ArgumentNullException(nameof(info));

           // TODO: implement deserialization

            BingSpellCheckService = ServiceLocator.Get<IBingSpellCheckService>();
        }
        #endregion
    }

To make the deserialization work, you need to use the ServiceLocator Antipattern. Warning: Use it only in the Serialization Constructor!

    public static class ServiceLocator
    {
        public static T Get<T>()
        {
            return (T)GlobalConfiguration.Configuration.DependencyResolver.GetService(typeof(T));
        }
    }

See also: CA2240: Implement ISerializable correctly

@MovGP0

This comment has been minimized.

MovGP0 commented Mar 5, 2018

There is also another solution, that can be much cleaner: don't use IDialog.

There is nothing that forces the use of dialogs. You can inject all the services you need into the controller and persist-and-retrieve the state manually using the repository of your choice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment