Closed wiltodelta closed 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?
@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?
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?
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.
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.
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();
I'm closing this issue for now - feel free to re-open if it hasn't been resolved.
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
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.
this is a sample that shows how to resolve services from dialog constructors and the container:
@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);
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?
@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
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?
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:
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();
}
}
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);
}
}
ApiController
: [BotAuthentication]
[Route("api/messages")]
public sealed class MessagesController : ApiController
{
public MessagesController(Func<RootDialog> rootDialogFactory)
{
RootDialogFactory = rootDialogFactory ?? throw new ArgumentNullException(nameof(rootDialogFactory));
}
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));
}
}
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.
How to use Dependency Injection with Dialogs?