BotFramework: Avoiding have to make everything [Serializable]

In the last article I touched on how IoC is used within botframework using Autofac.

If you’re comfortable with IoC, you probably started to enhance your dialogs using constructor injection, so that you can put your conversation logic outside of your entry point and just start coding without needing an implementation to exist yet, e.g.,

[Serializable]
public class ResolvingDialog : IDialog<object>
{
    private readonly IMessageRepository _messageRepository;

    public ResolvingDialog(IMessageRepository messageRepository)
    {
        _messageRepository = messageRepository;
    }

    public async Task StartAsync(IDialogContext context)
    {
        await context.PostAsync($"Hi!");
        context.Wait(MessageReceivedAsync);
    }

    public async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> argument)
    {
        var message = await argument;
        await _messageRepository.SaveMessage(message);

        await context.PostAsync($"working now");
        context.Wait(MessageReceivedAsync);
    }
}

Then you can just wire up the interface resolution by defining the default implementation for the interface in your IoC configuration e.g.

var builder = new ContainerBuilder();
builder.RegisterType<MessageRepository>().As<IMessageRepository>();

...

var container = builder.Build();

Easy, right? Exactly how you’d do it for a Controller in a WebAPI or MVC project.

However..

serializable all the things please!

Ah. This appears to be due to the dialog and all their dependencies needing to be serialized such that the conversation can avoid needing “sticky sessions” to keep the context between messages.

ye olde service locator pattern

One way to avoid this that I’ve been using is something like a Service Locator/Factory pattern.

Yes, people don’t like the Service Locator pattern, so if you’re one of them please do suggest alternative solutions! I’d love to hear them.

Instead of using constructor injection, we can define a static property on the dialog which is resolved via a factory class to retrieve the concrete implementation via our IoC configuration.

Lots of long words there, so let’s look at some code instead!

The dialog now has a property for each interface we need an implementation for, resolved using another class:

[Serializable]
public class ResolvingDialog : IDialog<object>
{
    private static readonly IMessageRepository _messageRepository
       = ServiceResolver.Get<IMessageRepository>();

    public async Task StartAsync(IDialogContext context)
    {
        await context.PostAsync($"Hi!");
        context.Wait(MessageReceivedAsync);
    }

    public async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> argument)
    {
        var message = await argument;
        await _messageRepository.SaveMessage(message);

        await context.PostAsync($"working now");
        context.Wait(MessageReceivedAsync);
    }
}

That ServiceResolver class merely hooks into AutoFac and returns a concrete implementation:

public static class ServiceResolver
{
    public static IContainer Container;

    public static T Get<T>()
    {
        using (var scope = Container.BeginLifetimeScope())
            return scope.Resolve<T>();
    }
}

Now we just set the resolver’s static Container in a bootstrapping class (or just in the Global.asax.cs):

var builder = new ContainerBuilder();
builder.RegisterType<MessageRepository>().As<IMessageRepository>();
...
var container = builder.Build();

ServiceResolver.Container = container;

Now running that again gives us:

not serializable? not a problem

static?! Service Locator?!

Yeah, the service resolution class is static. The properties in the dialog are static. I’m using a Service Locator pattern.

This may not seem clean, but it means these dependencies do not need to be [Serializable]. Having a Container property on ServiceResolver means we can pass in a container full of mocks/ stubs/ fakes/ substitutes to facilitate testing whilst using this static class.

Summary

We’ve now got a way of keeping the dialog classes reasonably empty, merely handling initial routing like a Controller would.

This should hopefully allow for a nicer structure in your bot code. I’d be interested in hearing how you’ve solved this problem in your own projects, since – let’s face it – I’m using a Service Locator and static all over the place..

8 thoughts on “BotFramework: Avoiding have to make everything [Serializable]

  1. Interesting, thanks for posting. Does the repository need to be static? I suppose as long as your repository is thread-safe then no issues.

  2. Only service classes should have dependencies, and service classes shouldn’t be serialized. If a service class is serialized, it will have to use service location.

    It’s counter-intuitive to me that they suggest Dialogs be serializable but I’m not familiar with the library. Is inheriting from MarshalByRefObject feasible?
    Do they intend that you do service location from the IDialogContext?

    I’d be curious to hear what the bot framework design team has to say about this.

  3. You can mark a property in the dialog as [NonSerializable] a DI object. If you did:

    [NonSerializable]
    private readonly IMessageRepository _messageRepository;

    I think it would work. It will not persist between requests is the drawback.

    • Correct, and this is something I’ve tried; it certainly appears to work up until you have a response that doesn’t explicitly hit the dialog’s constructor, such as a Prompt or a Card Action – then it fails due to the property being null since (as you noted) it doesn’t persist between requests. Booo!

    • Does not work because it will be always null because dialog instance never calls constructor as it always deserialized.

  4. You need to mark the properties of your services as private readonly. Then with Autofac registration, you should use FiberModule.Key_DoNotSerialize to indicate that instances of those services should not be serialized. This will allow you to use the normal style of coding, no need for Service Locator.

    That said, there seem to be a few issues with the current release where this doesn’t completely work as expected (see https://github.com/Microsoft/BotBuilder/issues/2515). That’s how I found your post 🙂

  5. Hi!

    I have tried your solution and it works very well in my root dialog. My bot seems to have issues when I extend the solution to a child dialog as it crashes with a timeout exception. Is there a thing I forgot to do with my child dialogs? Should I have all my child dialogs registered with autofac and call them with locator??

    Thanks a lot!

Leave a Reply to Rambalac Cancel reply

Your email address will not be published. Required fields are marked *