Bot Framework (C#) und Azure Functions

Im letzten Blogbeitrag habe ich gezeigt was Azure Functions sind, wie sie funktionieren und welche Tools es für C# Entwickler gibt. Diesmal erkläre ich warum sich Azure Functions für Bots gut eignen und was zu beachten ist, wenn man einen Bot mit Bot Framework und C# erstellen möchte.

Azure Functions für Bots

Mit Azure Functions lassen sich Micro Services oder eben Funktionen erstellen. Diese werden ereignisbasiert ausgeführt, nach den tatsächlich benötigten Ressourcen abgerechnet und skalieren automatisch.

Wenn man einen Bot betrachtet, besteht dieser in der Regel aus genau einer Funktion. Diese Funktion wird durch eine eingehende Nachricht, einem HTTP Request, aufgerufen. In der Funktion wird die Nachricht dann verarbeitet und eine Antwort zurückgeschickt.

Einen Bot zeichnet auch aus, dass dieser rund um die Uhr erreichbar sein muss und möglichst schnell auf Nachrichten eines Benutzer antworten soll. Natürlich gibt es Zeiten zu denen keine einzigen Nachrichten eingehen, aber es gibt Stoßzeiten zu denen viele Nutzer gleichzeitig Nachrichten schicken. Wenn zu solchen Stoßzeiten die Nachrichten nicht rasch genug beantwortet werden, springen die User sehr schnell ab.

Dank Azure Functions fallen keine Kosten an, wenn keine Nachrichten eingehen, und es stehen automatisch genügen Ressourcen zur Verfügung, wenn viele Benutzer mit dem Bot schreiben.

Wer mit der Entwicklung eines Bots beginnt und ohnehin wenig Zugriffe hat, braucht sich somit keine Gedanken um Fixkosten zu machen.

Bot Framework und Azure Functions

Das Bot Framework, bzw. um genau zu sein das Bot Builder SDK, ist auch in Azure Functions nutzbar. Da das Bot Builder SDK allerdings für ASP.NET entwickelt wurde, funktionieren gewisse Teile, wie zum Beispiel die Authentifizierung der Request, nicht mit Azure Functions. Microsoft bietet aber inzwischen die BotBuilder-Azure Library an, die genau diese Funktionen liefert.

Neben den Basis Features und Util-Methoden bietet BotBuilder-Azure zusätzlich noch Erweiterungen für andere Azure Dienste. So lässt sich der Bot State zum Beispiel in Azure Table Storage oder in Azure DocumentDBs speichern und man kann sich in eine Azure Table loggen.

Probleme beim Deserialisieren des States

Derzeit gibt es noch ein Problem, wenn man das C# Bot Builder SDK mit Azure Functions nutzen möchte. Für das Serialisieren und Deserialisieren des States wird im Bot Builder der Binary Formatter verwendet. Beim deserialisieren kommt es zu einem Fehler, wenn ein Typ aus einer Assembly benötigt wird, der noch nicht in die AppDomain geladen wurde. Grund dafür ist, dass beim Deserialisieren die Assembly im Startordner der aktuellen Anwendung gesucht wird. Die Runtime, die unserer Code läd und ausführt, liegt aber in einem anderen Verzeichnis als unsere Dateien. Daher wird die benötigte Assembly nicht gefunden und der State kann nicht wiederhergestellt werden.

Es gibt zu diesem Problem auch eine Diskussion auf GitHub.

Lösung

Nachdem ich mich ein wenig mit der Thematik auseinandergesetzt habe, konnte ich einen Workaround finden, mit dem man trotzdem schon jetzt Bots mit Azure Functions nutzen kann. Die folgende Lösung habe ich auch als Kommentar auf GitHub gepostet.

Wird eine Assembly die noch nicht geladen ist benötigt, wird das … Event der AppDomain ausgelöst. Im Event Handler besteht die Möglichkeit selbst zu definieren, wo nach der Assembly gesucht wird. Daher habe ich eine eigene Klasse erstellt, die sich auf dieses Event registriert und die Assembly aus dem gleichen Ordner läd, in dem auch die Assembly mit meiner Function liegt.

public class AzureFunctionsResolveAssembly : IDisposable
{
    public AzureFunctionsResolveAssembly()
    {
        AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
    }

    void IDisposable.Dispose()
    {
        AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve;
    }

    private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs arguments)
    {
        var assembly = AppDomain.CurrentDomain.GetAssemblies()
            .FirstOrDefault(a => a.GetName().FullName == arguments.Name);

        if (assembly != null)
        {
            return assembly;
        }

        // try to load assembly from file
        var assemblyDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
        var assemblyName = new AssemblyName(arguments.Name);
        var assemblyFileName = assemblyName.Name + ".dll";
        string assemblyPath;

        if (assemblyName.Name.EndsWith(".resources"))
        {
            var resourceDirectory = Path.Combine(assemblyDirectory, assemblyName.CultureName);
            assemblyPath = Path.Combine(resourceDirectory, assemblyFileName);
        }
        else
        {
            assemblyPath = Path.Combine(assemblyDirectory, assemblyFileName);
        }

        if (File.Exists(assemblyPath))
        {
            return Assembly.LoadFrom(assemblyPath);
        }

        return null;
    }
}

Um diesen eigenen Handler zu verwenden instanziiere ich ihn in einem „Using Statement“, das meinen gesamten Bot Code umschließt.

[FunctionName("Messages")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "messages")]HttpRequestMessage req, TraceWriter log)
{
    // use custom assembly resolve handler
    using(new AzureFunctionsResolveAssembly())
    using (BotService.Initialize())
    {
        // Deserialize the incoming activity
        string jsonContent = await req.Content.ReadAsStringAsync();
        var activity = JsonConvert.DeserializeObject<Activity>(jsonContent);

        // authenticate incoming request and add activity.ServiceUrl to MicrosoftAppCredentials.TrustedHostNames
        // if request is authenticated
        if (!await BotService.Authenticator.TryAuthenticateAsync(req, new[] { activity }, CancellationToken.None))
        {
            return BotAuthenticator.GenerateUnauthorizedResponse(req);
        }

        if (activity != null)
        {
            switch (activity.GetActivityType())
            {
                case ActivityTypes.Message:
                    await Conversation.SendAsync(activity, () => new RootDialog());
                    break;
                [...]
            }
        }
        return req.CreateResponse(HttpStatusCode.Accepted);
    }
}

Fazit

Azure Functions eignen sich auf Grund ihrer Eigenschaften (ereignisbasiert, Abrechnung nach tatsächlichen benötigter Ressourcen, automatische Skalierung) besonders gut für Bots. Mit der BotBuilder-Azure Library bietet Microsoft eine Ergänzung zum Bot Builder SDK für Azure Functions. Und das Problem beim Deserialisieren des States lässt sich mit dem gezeigten Workaround recht einfach lösen.

Speichere in deinen Favoriten diesen permalink.

Schreibe einen Kommentar