microsoft / botframework-sdk

Bot Framework provides the most comprehensive experience for building conversation applications.
MIT License
7.5k stars 2.44k forks source link

Dependency Injection #106

Closed wiltodelta closed 8 years ago

wiltodelta commented 8 years ago

How to use Dependency Injection with Dialogs?

willportnoy commented 8 years ago

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 commented 8 years ago

@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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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

Unders0n commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

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 commented 6 years ago

@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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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.