Implementieren von benutzerdefiniertem Speicher für Ihren BotImplement custom storage for your bot

gilt für: SDK v4APPLIES TO: SDK v4

Die Interaktionen eines Bots lassen sich in drei Bereiche unterteilen: erstens den Austausch von Aktivitäten mit dem Azure Bot Service, zweitens das Laden und Speichern des Dialogzustands mit einem Speicher und schließlich alle anderen Back-End-Dienste, mit denen der Bot zum Ausführen seiner Aufgabe arbeiten muss.A bot's interactions fall into three areas: firstly, the exchange of Activities with the Azure Bot Service, secondly, the loading and saving of dialog state with a Store, and finally any other back-end services the bot needs to work with to get its job done.

Diagramm zur Interaktion mit der aufskalierenden Ebene

VoraussetzungenPrerequisites

  • Den vollständigen Beispielcode, der in diesem Artikel verwendet wird, finden Sie hier: C#-Beispiel.The full sample code used in this article can be found here: C# sample.

In diesem Artikel wird die Semantik im Zusammenhang mit den Interaktionen des Bots mit dem Azure Bot Service und dem Speicher behandelt.In this article, we will be exploring the semantics around the bot's interactions with the Azure Bot Service and the Store.

Bot Framework enthält eine Standardimplementierung, die die Anforderungen vieler Anwendungen erfüllt und zu deren Verwendung nur einige Zeilen Initialisierungscode zum Zusammenfügen der einzelnen Komponenten erforderlich sind.The Bot Framework includes a default implementation; this implementation will most likely fit the needs of many applications, and all that is needed to be done to make use of it is to plug the pieces together with a few lines of initialization code. Viele der Beispiele veranschaulichen diese Implementierung.Many of the samples illustrate just that.

In diesem Artikel soll jedoch erläutert werden, was Sie tun können, wenn die Semantik der Standardimplementierung in Ihrer Anwendung nicht wie gewünscht funktioniert.The goal here, however, is to describe what you can do when the semantics of the default implementation doesn't quite work as you might like in your application. Der springende Punkt ist, dass dies ein Framework ist und keine vordefinierte Anwendung mit einem festen Verhalten. Anders ausgedrückt: Die Implementierung vieler der Mechanismen im Framework ist lediglich die Standardimplementierung und nicht die einzige Implementierung.The basic point is that this is a framework and not a canned application with fixed behavior, in other words, the implementation of many of the mechanisms in the framework is just the default implementation and not the only implementation.

Insbesondere gibt das Framework nicht die Beziehung zwischen dem Austausch von Aktivitäten mit dem Azure Bot Service und dem Laden und Speichern eines Botzustands vor. Es stellt lediglich eine Standardimplementierung bereit.Specifically, the framework does not dictate the relationship between the exchange of Activities with the Azure Bot Service and the loading and saving of any Bot state; it simply provides a default. Um diesen Punkt zu verdeutlichen, entwickeln wir eine alternative Implementierung mit einer anderen Semantik.To illustrate this point further, we will be developing an alternative implementation that has different semantics. Die alternative Lösung ist ebenso gut in das Framework integriert und möglicherweise sogar besser geeignet für die entwickelte Anwendung.This alternative solution sits equally well in the framework and may even be more appropriate for the application being developed. Dies hängt einzig und allein vom Szenario ab.It all depends on the scenario.

Verhalten der standardmäßigen BotFrameworkAdapter- und SpeicheranbieterBehavior of the default BotFrameworkAdapter and Storage providers

Zuerst sehen wir uns die im folgenden Sequenzdiagramm dargestellte Standardimplementierung an, die in den Frameworkpaketen enthalten ist:Firstly, let's review the default implementation that ships as part of the framework packages as shown by the following sequence diagram:

Standarddiagramm für das aufskalieren

Beim Empfang einer Aktivität lädt der Bot den Zustand, der dieser Konversation entspricht.On receiving an Activity, the bot loads the state corresponding to this conversation. Anschließend führt er die Dialoglogik mit diesem Zustand und der soeben empfangenen Aktivität aus.It then runs the dialog logic with this state and the Activity that has just arrived. Während der Ausführung des Dialogs werden eine oder mehrere ausgehende Aktivitäten erstellt und sofort gesendet.In the process of executing the dialog, one or more outbound activities are created and immediately sent. Wenn die Verarbeitung des Dialogs abgeschlossen ist, speichert der Bot den aktualisierten Zustand, wobei der alte Zustand mit dem neuen überschrieben wird.When the processing of the dialog is complete, the bot saves the updated state, overwriting the old state with new.

Bei diesem Verhalten können einige Probleme auftreten, die es zu erwägen gilt.It is worth considering a couple of things that can go wrong with this behavior.

Erstens: Wenn beim Speichervorgang aus irgendeinem Grund ein Fehler auftritt, ist der Zustand nicht mehr mit der Anzeige im Kanal synchron, da für den Benutzer, der die Antworten gesehen hat, der Eindruck entsteht, dass sich der Zustand verändert hat, obwohl dies nicht der Fall ist.Firstly, if the Save operation were to fail for some reason the state has implicitly slipped out of sync with what is seen on the channel because the user having seen the responses is under the impression that the state has moved forward, but it hasn't. Dies ist generell schlechter als das erfolgreiche Speichern des Zustands und ein erfolgreiches Antwortmessaging.This is generally worse than if the state was successful and the response messaging were successful. Dieses Problem kann Auswirkungen auf den Entwurf der Konversation haben: Der Dialog kann beispielsweise einen zusätzlichen, andernfalls redundanten Austausch von Bestätigungen mit dem Benutzer enthalten.This can have implications for the conversation design: for example, the dialog might include additional, otherwise redundant confirmation exchanges with the user.

Zweitens: Wenn die Implementierung horizontal hochskaliert auf mehreren Knoten bereitgestellt wird, kann der Zustand versehentlich überschrieben werden. Dies kann besonders verwirrend sein, da der Dialog wahrscheinlich Aktivitäten mit Bestätigungsnachrichten an den Kanal gesendet hat.Secondly, if the implementation is deployed scaled out across multiple nodes, the state can accidentally get overwritten - this can be particularly confusing because the dialog will likely have sent activities to the channel carrying confirmation messages. Nehmen Sie das Beispiel eines Pizzabestellungsbots: Wenn der Benutzer bei der Frage nach dem Belag Pilze und dann ohne Verzögerung Käse hinzufügt, können in einem horizontal hochskalierten Szenario mit mehreren aktiven Instanzen nachfolgende Aktivitäten gleichzeitig an verschiedene Computer gesendet werden, auf denen der Bot ausgeführt wird.Consider the example of a pizza order bot, if the user, on being asked for a topping, adds mushroom and without delay adds cheese, in a scaled-out scenario with multiple instances running subsequent activities can be sent concurrently to different machines running the bot. In diesem Fall kommt es zu einer so genannten „Racebedingung“, bei der ein Computer den von einem anderen Computer geschriebenen Zustand überschreiben kann.When this happens, there is what is referred to as a "race condition" where one machine might overwrite the state written by another. Da die Antworten in diesem Szenario bereits gesendet wurden, hat der Benutzer jedoch die Bestätigung erhalten, dass sowohl Pilze als auch Käse hinzugefügt wurden.However, in our scenario, because the responses were already sent, the user has received confirmation that both mushroom and cheese were added. Die gelieferte Pizza ist jedoch leider nur mit Pilzen oder nur mit Käse belegt, aber nicht mit beidem.Unfortunately, when the pizza arrives, it will only contain mushroom or cheese, not both.

Optimistische SperreOptimistic locking

Die Lösung besteht darin, eine Sperre für den Status zu verwenden.The solution is to introduce some locking around the state. Der Sperrentyp, den wir hier verwenden, wird als optimistische Sperre bezeichnet, da alle Prozesse so ausgeführt werden, als wären sie die jeweils einzigen aktiven Prozesse. Nach Abschluss der Verarbeitung wird dann eine Erkennung von Parallelitätsverletzungen ausgeführt.The particular style of locking we will be using here is called optimistic locking because we will let everything run as if they were each the only thing running and then we will detect any concurrency violations after the processing has been done. Dies mag kompliziert klingen, ist aber mithilfe von Cloudspeichertechnologien und den richtigen Erweiterungspunkten in Bot Framework sehr einfach umzusetzen.This may sound complicated but is very easy to build using cloud storage technologies and the right extension points in the bot framework.

Wir verwenden einen HTTP-Standardmechanismus, der auf dem Entitätstagheader (ETag) basiert.We will use a standard HTTP mechanism based on the entity tag header, (ETag). Das Verständnis dieses Mechanismus ist entscheidend, um den folgenden Code zu verstehen.Understanding this mechanism is crucial to understanding the code that follows. Das unten stehende Diagramm veranschaulicht die Sequenz.The following diagram illustrates the sequence.

Diagramm: Fehler bei der Vorbedingung für das aufskalieren

Das Diagramm zeigt eine Situation, in der zwei Clients eine Ressource aktualisieren.The diagram illustrates the case of two clients that are performing an update to some resource. Wenn ein Client eine GET-Anforderung ausgibt und eine Ressource vom Server zurückgegeben wird, enthält sie einen ETag-Header.When a client issues a GET request and a resource is returned from the server, it is accompanied by an ETag header. Der ETag-Header ist ein nicht transparenter Wert, der den Zustand der Ressource darstellt.The ETag header is an opaque value that represents the state of the resource. Wenn eine Ressource geändert wird, wird das ETag aktualisiert.If a resource is changed, the ETag will be updated. Nachdem der Client den Zustand aktualisiert hat, sendet er die Ressource mit einer POST-Anforderung an den Server zurück, wobei er den zuvor empfangenen ETag-Wert in einem If-Match-Vorbedingungsheader an die Anforderung anfügt.When the client has done its update to the state, it POSTs it back to the server, making this request the client attaches the ETag value it had previously received in a precondition If-Match header. Stimmt dieses ETag nicht mit dem zuletzt vom Server (bei allen Antworten an beliebige Clients) zurückgegebenen Wert überein, schlägt die Überprüfung der Vorbedingung mit einem Vorbedingungsfehler vom Typ 412 fehl.If this ETag does not match the value, the server last returned (on any response, to any client) the precondition check fails with a 412 Precondition Failure. Für den Client, der die POST-Anforderung sendet, ist dieser Fehler ein Indikator dafür, dass die Ressource aktualisiert wurde.This failure is an indicator to the client making the POST request that the resource has been updated. Das typische Verhalten für einen Client beim Auftreten dieses Fehlers ist das erneute Abrufen der Ressource mit GET, Anwenden der gewünschten Aktualisierung und Zurücksenden der Ressource mit POST.On seeing this failure, the typical behavior for a client will be to GET the resource again, apply the update it wanted, and POST the resource back. Diese zweite POST-Anforderung ist erfolgreich, sofern kein anderer Client die Ressource aktualisiert hat. Sollte dies der Fall sein, muss der Client den Vorgang einfach wiederholen.This second POST will be successful, assuming of course, that no other client has come and updated the resource, and if it has the client will just have to try again.

Dieser Prozess wird als „optimistisch“ bezeichnet, weil der Client, der eine Ressource abgerufen hat, seine Verarbeitung fortsetzt. Die Ressource selbst wird nicht „gesperrt“ in dem Sinn, dass andere Clients ohne Einschränkung auf sie zugreifen können.This process is called "optimistic" because the client, having got hold of a resource proceeds to do its processing, the resource itself is not "locked" in the sense that other clients can access it without any restriction. Konflikte zwischen Clients bezüglich des Zustands der Ressource sollten erst nach Abschluss der Verarbeitung ermittelt werden.Any contention between clients over what the state of the resource should be is not determined until the processing has been done. Im Allgemeinen ist diese Strategie in einem verteilten System besser geeignet als der entgegengesetzte „pessimistische“ Ansatz.As a rule, in a distributed system this strategy is more optimal than the opposite "pessimistic" approach.

Der oben beschriebene optimistische Sperrmechanismus setzt voraus, dass die Programmlogik sicher wiederholt werden kann. Entscheidend ist hierbei selbstverständlich, was mit externen Dienstaufrufen passiert.The optimistic locking mechanism we've covered assumes program logic can be safely retried, needless, to say the important thing to consider here is what happens to external service calls. Die ideale Lösung wären idempotente Dienste.The ideal solution here is if these services can be made idempotent. In der Informatik ist ein idempotenter Vorgang ein Vorgang, der keine weiteren Auswirkungen hat, wenn er mehrmals mit den gleichen Eingabeparametern aufgerufen wird.In computer science, an idempotent operation is one that has no additional effect if it is called more than once with the same input parameters. Dies trifft auf HTTP-REST-Dienste zu, die GET, PUT und DELETE implementieren.Pure HTTP REST services that implement GET, PUT and DELETE fit this description. Der Grund hierfür liegt auf der Hand: Da die Verarbeitung möglicherweise wiederholt wird, ist es von Vorteil, wenn alle erforderlichen Aufrufe, die im Rahmen dieser Wiederholung erneut ausgeführt werden, keine zusätzlichen Auswirkungen haben.The reasoning here is intuitive: we might be retrying the processing and so making any calls it needs to make have no additional effect as they are re-executed as part of that retry is a good thing. In dieser Erörterung gehen wir vom Idealfall aus und setzen voraus, dass die auf der rechten Seite des Systemdiagramms am Anfang des Artikels dargestellten Back-End-Dienste alle idempotente HTTP-REST-Dienste sind. Im verbleibenden Teil dieses Artikels konzentrieren wir uns daher ausschließlich auf den Austausch von Aktivitäten.For the sake of this discussion, we will assume we are living in an ideal world and the backend services shown to the right of the system picture at the beginning of this article are all idempotent HTTP REST services, from here on we will focus only on the exchange of activities.

Puffern von ausgehenden AktivitätenBuffering outbound activities

Das Senden einer Aktivität ist kein idempotenter Vorgang, und zudem ist nicht klar, ob dies im End-to-End-Szenario sinnvoll wäre.The sending of an Activity is not an idempotent operation, nor is it clear that would make much sense in the end-to-end scenario. Im Grunde genommen enthält die Aktivität oftmals nur eine Nachricht, die an eine Ansicht angefügt oder vielleicht von einem Sprachsynthese-Agent gesprochen wird.After all the Activity is often just carrying a message that is appended to a view or perhaps spoken by a text to speech agent.

Beim Senden der Aktivitäten soll in erster Linie vermieden werden, dass diese mehrmals gesendet werden.The key thing we want to avoid with sending the activities is sending them multiple times. Das Problem hierbei ist, dass der optimistische Sperrmechanismus die mehrmalige erneute Ausführung der Logik erfordert.The problem we have is that the optimistic locking mechanism requires that we with rerun our logic possibly multiple times. Die Lösung ist einfach: Die ausgehenden Aktivitäten des Dialogs müssen gepuffert werden, bis sicher ist, dass die Logik nicht erneut ausgeführt wird.The solution is simple: we must buffer the outbound activities from the dialog until we are sure we are not going to rerun the logic. Die Aktivitäten müssen folglich bis zum erfolgreichen Abschluss eines Speichervorgangs gepuffert werden.That is until after we have a successful Save operation. Der gewünschte Ablauf sieht in etwa wie folgt aus:We are looking for a flow that looks something like the following:

Diagramm des Puffers für das aufskalieren

Sofern eine Wiederholungsschleife für die Ausführung des Dialogs erstellt werden kann, erhalten wir das folgende Verhalten, wenn beim Speichervorgang ein Vorbedingungsfehler auftritt:Assuming we can build a retry loop around the dialog execution we get the following behavior when there is a precondition failure on the Save operation:

Diagramm zum Speichern von Skalierungs- und Skalierungsskalieren

Wird dieser Mechanismus im obigen Beispiel angewendet, sollte niemals eine falsch positive Bestätigung für einen zu einer Bestellung hinzugefügten Pizzabelag erfolgen.Applying this mechanism and revisiting our example from earlier we should never see an erroneous positive acknowledgment of a pizza topping being added to an order. Auch wenn wir die Bereitstellung auf mehrere Computer horizontal hochskalieren, erzielen wir mit dem optimistischen Sperrschema eine effektive Serialisierung der Zustandsaktualisierungen.In fact, although we might have scaled out our deployment across multiple machines, we have effectively serialized our state updates with the optimistic locking scheme. Beim Pizzabestellungsbot kann die Bestätigung für das Hinzufügen eines Belags jetzt so geschrieben werden, dass der vollständige Zustand korrekt wiedergegeben wird.In our pizza ordering but the acknowledgement from adding an item can now even be written to reflect the full state accurately. Wenn der Benutzer beispielsweise „Käse“ und dann, bevor der Bot antworten konnte, sofort „Pilze“ eingibt, können die beiden Antworten nun „Pizza mit Käse“ und anschließend „Pizza mit Käse und Pilzen“ lauten.For example, if the user immediately types "cheese" and then before the bot has had a chance to reply "mushroom" the two replies can now be "pizza with cheese" and then "pizza with cheese and mushroom."

Im Sequenzdiagramm können Sie sehen, dass die Antworten nach einem erfolgreichen Speichervorgang verloren gehen können. Sie können jedoch an jeder beliebigen Stelle in der End-to-End-Kommunikation verloren gehen.Looking at the sequence diagram we can see that the replies could be lost after a successful Save operation, however, they could be lost anywhere in the end to end communication. Der springende Punkt ist, dass dieses Problem nicht durch die Infrastruktur für die Zustandsverwaltung behoben werden kann.The point is this is not a problem the state management infrastructure can fix. Das Problem erfordert ein übergeordnetes Protokoll, möglicherweise unter Einbeziehung des Benutzers des Kanals.It will require a higher-level protocol and possibly one involving the user of the channel. Wenn für den Benutzer der Eindruck entsteht, dass der Bot nicht geantwortet hat, ist beispielsweise zu erwarten, dass der Benutzer den Vorgang wiederholt oder sich ähnlich verhält.For example, if the bot appears to the user not to have replied it is reasonable to expect the user to ultimately try again or some such behavior. Gelegentliche vorübergehende Ausfälle können in einem Szenario zu erwarten sein. Deutlich unrealistischer ist jedoch die Annahme, dass ein Benutzer falsch positive Bestätigungen oder andere unbeabsichtigte Nachrichten herausfiltern kann.So while it is reasonable for a scenario to have occasional transient outages such as this it is far less reasonable to expect a user to be able to filter out erroneous positive acknowledgements or other unintended messages.

In der neuen benutzerdefinierten Speicherlösung setzen wir all dies mit drei Schritten bzw. Teilen um, die in der Standardimplementierung im Framework nicht enthalten sind.Pulling this all together, in our new custom storage solution, we are going to do three things the default implementation in the framework doesn't do. Erstens: Wir verwenden ETags, um Konflikte zu erkennen. Zweitens: Wir wiederholen die Verarbeitung, wenn der ETag-Fehler erkannt wird. Drittens: Wir puffern alle ausgehenden Aktivitäten, bis ein erfolgreicher Speichervorgang stattgefunden hat.Firstly, we are going to use ETags to detect contention, secondly we are going to retry the processing when the ETag failure is detected and thirdly we are going to buffer any outbound Activities until we have a successful save. Im weiteren Verlauf dieses Artikels wird die Implementierung dieser drei Teile beschrieben.The remainder of this article describes the implementation of these three parts.

Implementieren der ETag-UnterstützungImplementing ETag Support

Wir beginnen damit, eine Schnittstelle für unseren neuen Store mit ETag-Unterstützung zu definieren.We start out by defining an interface for our new store with ETag support. Mithilfe der Schnittstelle können die Mechanismen zur Abhängigkeitsinjektion in ASP.NET ganz einfach genutzt werden.The interface will make it very easy to leverage the dependency injection mechanisms we have in ASP.NET. Die Schnittstelle bedeutet, dass wir eine Version für die Produktion implementieren können.Having the interface means we can implement a version for production. Wir könnten auch eine Version für Komponententests implementieren, die im Arbeitsspeicher ausgeführt werden, ohne das Netzwerk zu treffen.We could also implement a version for unit tests that runs in memory without the need of hitting the network.

Die Schnittstelle besteht aus Load- und Save-Methoden.The interface consists of Load and Save methods. Beide Methoden akzeptieren den Schlüssel, der für den Zustand verwendet wird.Both these take the key we will use for the state. Die Load-Methode gibt die Daten und das zugehörige ETag zurück.The Load will return the data and the associated ETag. Die Save-Methode akzeptiert diese Daten und das ETag.And the Save will take these in. Außerdem gibt die Save-Methode einen booleschen Wert zurück.Additionally, the Save will return bool. Dieser boolesche Wert gibt an, ob das ETag abgeglichen wurde und der Speichervorgang erfolgreich war.This bool will indicate whether the ETag has matched and the Save was successful. Er soll nicht als allgemeiner Fehlerindikator dienen, sondern als ein spezifischer Indikator für Vorbedingungsfehler. Wir modellieren den Wert nicht als Ausnahme, sondern als einen Rückgabecode, weil wir die zugehörige Ablaufsteuerungslogik in Form einer Wiederholungsschleife schreiben.This is not intended as a general error indicator but rather a specific indicator of precondition failure, we model this as a return code rather than an exception because we will be writing control flow logic around this in the shape of our retry loop.

Da diese Speicherkomponente auf niedrigster Ebene austauschbar sein soll, stellen wir sicher, dass keine Serialisierungsanforderungen für sie gelten. Wir wollen jedoch festlegen, dass der Inhalt in JSON gespeichert wird, damit der Inhaltstyp von einer Speicherimplementierung festgelegt werden kann.As we would like this lowest level storage piece to be pluggable, we will make sure to avoid placing any serialization requirements on it, however we would like to specify that the content save should be JSON, that way a store implementation can set the content-type. Die einfachste und naheliegendste Methode hierzu ist in .NET die Verwendung von Argumenttypen. Insbesondere typisieren wir das Inhaltsargument als JObject.The easiest and most natural way to do this in .NET is through the argument types, specifically we will type the content argument as JObject. In JavaScript oder TypeScript ist dies nur ein reguläres natives Objekt.In JavaScript or TypeScript this will just be a regular native object.

Die resultierende Schnittstelle sieht wie folgt aus:This is the resulting interface:

IStore.csIStore.cs

public interface IStore
{
    Task<(JObject content, string etag)> LoadAsync(string key);

    Task<bool> SaveAsync(string key, JObject content, string etag);
}

Die Implementierung dieser Lösung für Azure Blob Storage ist unkompliziert.Implementing this against Azure Blob Storage is straight forward.

BlobStore.csBlobStore.cs

public class BlobStore : IStore
{
    private readonly CloudBlobContainer _container;

    public BlobStore(string accountName, string accountKey, string containerName)
    {
        if (string.IsNullOrWhiteSpace(accountName))
        {
            throw new ArgumentException(nameof(accountName));
        }

        if (string.IsNullOrWhiteSpace(accountKey))
        {
            throw new ArgumentException(nameof(accountKey));
        }

        if (string.IsNullOrWhiteSpace(containerName))
        {
            throw new ArgumentException(nameof(containerName));
        }

        var storageCredentials = new StorageCredentials(accountName, accountKey);
        var cloudStorageAccount = new CloudStorageAccount(storageCredentials, useHttps: true);
        var client = cloudStorageAccount.CreateCloudBlobClient();
        _container = client.GetContainerReference(containerName);
    }

    public async Task<(JObject content, string etag)> LoadAsync(string key)
    {
        if (string.IsNullOrWhiteSpace(key))
        {
            throw new ArgumentException(nameof(key));
        }

        var blob = _container.GetBlockBlobReference(key);
        try
        {
            var content = await blob.DownloadTextAsync();
            var obj = JObject.Parse(content);
            var etag = blob.Properties.ETag;
            return (obj, etag);
        }
        catch (StorageException e)
            when (e.RequestInformation.HttpStatusCode == (int)HttpStatusCode.NotFound)
        {
            return (new JObject(), null);
        }
    }

    public async Task<bool> SaveAsync(string key, JObject obj, string etag)
    {
        if (string.IsNullOrWhiteSpace(key))
        {
            throw new ArgumentException(nameof(key));
        }

        if (obj == null)
        {
            throw new ArgumentNullException(nameof(obj));
        }

        var blob = _container.GetBlockBlobReference(key);
        blob.Properties.ContentType = "application/json";
        var content = obj.ToString();
        if (etag != null)
        {
            try
            {
                await blob.UploadTextAsync(content, Encoding.UTF8, new AccessCondition {IfMatchETag = etag}, new BlobRequestOptions(), new OperationContext());
            }
            catch (StorageException e)
                when (e.RequestInformation.HttpStatusCode == (int)HttpStatusCode.PreconditionFailed)
            {
                return false;
            }
        }
        else
        {
            await blob.UploadTextAsync(content);
        }

        return true;
    }
}

Wie Sie sehen, übernimmt Azure Blob Storage die eigentliche Arbeit.As you can see Azure Blob Storage is doing the real work here. Beachten Sie den Catch-Block für bestimmte Ausnahmen und wie dies umgesetzt wird, um die Erwartungen des aufrufenden Codes zu erfüllen.Note the catch of specific exceptions and how that is translated across to meet what will be the expectations of the calling code. Beim Laden soll eine Ausnahme vom Typ „Nicht gefunden“ NULL zurückgeben, und die Ausnahme für den Vorbedingungsfehler beim Speichern soll einen booleschen Wert zurückgeben.That is, on the load we want a Not Found exception to return null and the Precondition Failed exception on the Save to return bool.

Der vollständige Quellcode ist in einem entsprechenden Beispiel verfügbar, das eine Arbeitsspeicherimplementierung enthält.All this source code will be available in a corresponding sample and that sample will include a memory store implementation.

Implementieren der WiederholungsschleifeImplementing the Retry Loop

Die grundlegende Form der Schleife leitet sich direkt von dem Verhalten ab, das in den Sequenzdiagrammen dargestellt ist.The basic shape of the loop is derived directly from the behavior shown in the sequence diagrams.

Beim Empfang einer Aktivität erstellen wir einen Schlüssel für den entsprechenden Zustand für diese Konversation.On receiving an Activity we create a key for the corresponding state for that conversation. Wir ändern die Beziehung zwischen der Aktivität und dem Konversationszustand nicht und erstellen den Schlüssel daher auf die gleiche Weise wie bei der Standardzustandsimplementierung.We are not changing the relationship between Activity and conversation state, so we will be creating the key in exactly the same way as in the default state implementation.

Nachdem wir den geeigneten Schlüssel erstellt haben, versuchen wir, den entsprechenden Zustand zu laden.After having created the appropriate key we will attempt to Load the corresponding state. Anschließend führen wir die Dialoge des Bots aus und versuchen, die Informationen zu speichern.Then run the bot's dialogs and then attempt to Save. Wenn der Speichervorgang erfolgreich ist, senden wir die aus der Ausführung des Dialogs resultierenden ausgehenden Aktivitäten, womit der Vorgang abgeschlossen ist.If that Save is successful, we will send the outbound Activities that resulted from running the dialog and be done. Tritt beim Speichern ein Fehler auf, gehen wir zurück und wiederholen den gesamten Prozess vor dem Laden.Otherwise we will go back and repeat the whole process from before the Load. Durch die Wiederholung des Ladevorgangs erhalten wir ein neues ETag, sodass der nächste Speichervorgang hoffentlich erfolgreich ist.Redoing the Load will give us a new ETag and so next time the Save will hopefully be successful.

Die resultierende OnTurn-Implementierung sieht wie folgt aus:The resulting OnTurn implementation looks like this:

ScaleoutBot.csScaleoutBot.cs

protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
    // Create the storage key for this conversation.
    var key = $"{turnContext.Activity.ChannelId}/conversations/{turnContext.Activity.Conversation?.Id}";

    // The execution sits in a loop because there might be a retry if the save operation fails.
    while (true)
    {
        // Load any existing state associated with this key
        var (oldState, etag) = await _store.LoadAsync(key);

        // Run the dialog system with the old state and inbound activity, the result is a new state and outbound activities.
        var (activities, newState) = await DialogHost.RunAsync(_dialog, turnContext.Activity, oldState, cancellationToken);

        // Save the updated state associated with this key.
        var success = await _store.SaveAsync(key, newState, etag);

        // Following a successful save, send any outbound Activities, otherwise retry everything.
        if (success)
        {
            if (activities.Any())
            {
                // This is an actual send on the TurnContext we were given and so will actual do a send this time.
                await turnContext.SendActivitiesAsync(activities, cancellationToken);
            }

            break;
        }
    }
}

Beachten Sie, dass die Dialogausführung als Funktionsaufruf modelliert wurde.Note that we have modeled the dialog execution as a function call. Eine komplexere Implementierung würde vielleicht eine Schnittstelle umfassen und diese Abhängigkeit als injizierbar festlegen. In diesem Fall betont die Platzierung des Dialogs hinter einer statischen Funktion jedoch die Funktionalität unseres Ansatzes.Perhaps a more sophisticated implementation would have defined an interface and made this dependency injectable but for our purposes having the dialog all sit behind a static function emphasize the functional nature of our approach. Indem wir die Implementierung so organisieren, dass die wichtigsten Teile funktionsfähig sind, schaffen wir uns eine gute Ausgangsposition für ihre erfolgreiche Verwendung in Netzwerken.As a general statement, organizing our implementation such that the crucial parts become functional puts us in a very good place when it comes to having it work successfully on networks.

Implementieren der Pufferung von ausgehenden AktivitätenImplementing outbound Activity buffering

Die nächste Anforderung besteht darin, ausgehende Aktivitäten bis zur erfolgreichen Speicherung zu puffern.The next requirement is that we buffer outbound Activities until a successful Save has been performed. Dies erfordert eine benutzerdefinierte BotAdapter-Implementierung.This will require a custom BotAdapter implementation. In diesem Code implementieren wir die abstrakte SendActivity-Funktion, um die Aktivität einer Liste hinzuzufügen, anstatt sie zu senden.In this code, we will implement the abstract SendActivity function to add the Activity to a list rather than sending it. Der von uns gehostete Dialog verfügt nicht über mehr Informationen.The dialog we will be hosting will be non-the-wiser. UpdateActivity- und DeleteActivity-Vorgänge werden in diesem speziellen Szenario nicht unterstützt und lösen daher nur eine Meldung vom Typ „Nicht implementiert“ von diesen Methoden aus.In this particular scenario UpdateActivity and DeleteActivity operations are not supported and so will just throw Not Implemented from those methods. Der Rückgabewert des SendActivity-Vorgangs ist für uns ebenfalls nicht relevant.We also don't care about the return value from the SendActivity. Dieser Wert wird von einigen Kanälen in Szenarien genutzt, in denen Aktualisierungen an Aktivitäten gesendet werden müssen (beispielsweise um Schaltflächen auf Karten zu deaktivieren, die im Kanal angezeigt werden).This is used by some channels in scenarios where updates to Activities need to be sent, for example, to disable buttons on cards displayed in the channel. Dieser Nachrichtenaustausch kann insbesondere dann kompliziert sein, wenn der Zustand erforderlich ist (dies wird in diesem Artikel nicht beschrieben).These message exchanges can get complicated particularly when state is required, that is outside the scope of this article. Die vollständige Implementierung des benutzerdefinierten BotAdapter sieht wie folgt aus:The full implementation of the custom BotAdapter looks like this:

DialogHostAdapter.csDialogHostAdapter.cs

public class DialogHostAdapter : BotAdapter
{
    private List<Activity> _response = new List<Activity>();

    public IEnumerable<Activity> Activities => _response;

    public override Task<ResourceResponse[]> SendActivitiesAsync(ITurnContext turnContext, Activity[] activities, CancellationToken cancellationToken)
    {
        foreach (var activity in activities)
        {
            _response.Add(activity);
        }

        return Task.FromResult(new ResourceResponse[0]);
    }

    #region Not Implemented
    public override Task DeleteActivityAsync(ITurnContext turnContext, ConversationReference reference, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public override Task<ResourceResponse> UpdateActivityAsync(ITurnContext turnContext, Activity activity, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
    #endregion
}

IntegrationIntegration

Jetzt müssen Sie diese verschiedenen neuen Teile nur noch in den vorhandenen Komponenten des Frameworks zusammenfügen.All that is left to do is glue these various new pieces together and plug them into the existing framework pieces. Die primäre Wiederholungsschleife befindet sich in der IBot OnTurn-Funktion.The main retry loop just sits in the IBot OnTurn function. Sie enthält unsere benutzerdefinierte IStore-Implementierung, die wir zu Testzwecken als abhängigkeitsinjizierbar festgelegt haben.It holds our custom IStore implementation which for testing purposes we have made dependency injectable. Wir haben den gesamten Dialoghostingcode in eine Klasse namens „DialogHost“ eingefügt, die eine einzelne öffentliche statische Funktion verfügbar macht.We have put all the dialog hosting code into a class called DialogHost that exposes a single public static function. Diese Funktion ist so definiert, dass sie die eingehende Aktivität und den alten Zustand akzeptiert und dann die resultierenden Aktivitäten und den neuen Zustand zurückgibt.This function is defined to take the inbound Activity and the old state and then return the resulting Activities and new state.

Zunächst erstellen wir in dieser Funktion den benutzerdefinierten BotAdapter, der weiter oben in diesem Artikel vorgestellt wurde.The first thing to do in this function is to create the custom BotAdapter we introduced earlier. Anschließend führen wir den Dialog einfach wie gewohnt aus, indem wir eine DialogSet- und DialogContext-Klasse erstellen und den üblichen „Weiter“- oder „Starten“-Fluss ausführen.Then we will just run the dialog in exactly the same was as we usually do by creating a DialogSet and DialogContext and doing the usual Continue or Begin flow. Der einzige Teil, mit dem wir uns nicht befasst haben, ist die Notwendigkeit eines benutzerdefinierten Accessors.The only piece we haven't covered is the need for a custom Accessor. Dieser Accessor ist ein sehr einfacher Shim, der das Übergeben des Dialogzustands an das Dialogsystem ermöglicht.This turns out to be a very simple shim that facilitates passing the dialog state into the dialog system. Der Accessor nutzt bei der Verwendung mit dem Dialogsystem Verweissemantik, sodass nur das Handle übergeben werden muss.The Accessor uses ref semantics when working with the dialog system and so all that is needed is to pass the handle across. Um dies zu verdeutlichen, haben wir die verwendete Klassenvorlage auf Verweissemantik beschränkt.To make things even clearer we have constrained the class template we are using to ref semantics.

Bei der Erstellung der Ebenen gehen wir vorsichtig vor. Wir fügen die JsonSerialization inline in den Hostingcode ein, da sie nicht in der austauschbaren Speicherebene enthalten sein soll, wenn verschiedene Implementierungen die Serialisierung auf unterschiedliche Weise vornehmen können.We are being very cautious in the layering, we are putting the JsonSerialization inline in our hosting code because we didn't want it inside the pluggable storage layer when different implementations might serialize differently.

Hier sehen Sie den Treibercode:Here is the driver code:

DialogHost.csDialogHost.cs

public static class DialogHost
{
    // The serializer to use. Moving the serialization to this layer will make the storage layer more pluggable.
    private static readonly JsonSerializer StateJsonSerializer = new JsonSerializer() { TypeNameHandling = TypeNameHandling.All };

    /// <summary>
    /// A function to run a dialog while buffering the outbound Activities.
    /// </summary>
    /// <param name="dialog">THe dialog to run.</param>
    /// <param name="activity">The inbound Activity to run it with.</param>
    /// <param name="oldState">Th eexisting or old state.</param>
    /// <returns>An array of Activities 'sent' from the dialog as it executed. And the updated or new state.</returns>
    public static async Task<(Activity[], JObject)> RunAsync(Dialog dialog, IMessageActivity activity, JObject oldState, CancellationToken cancellationToken)
    {
        // A custom adapter and corresponding TurnContext that buffers any messages sent.
        var adapter = new DialogHostAdapter();
        var turnContext = new TurnContext(adapter, (Activity)activity);

        // Run the dialog using this TurnContext with the existing state.
        var newState = await RunTurnAsync(dialog, turnContext, oldState, cancellationToken);

        // The result is a set of activities to send and a replacement state.
        return (adapter.Activities.ToArray(), newState);
    }

    /// <summary>
    /// Execute the turn of the bot. The functionality here closely resembles that which is found in the
    /// IBot.OnTurnAsync method in an implementation that is using the regular BotFrameworkAdapter.
    /// Also here in this example the focus is explicitly on Dialogs but the pattern could be adapted
    /// to other conversation modeling abstractions.
    /// </summary>
    /// <param name="dialog">The dialog to be run.</param>
    /// <param name="turnContext">The ITurnContext instance to use. Note this is not the one passed into the IBot OnTurnAsync.</param>
    /// <param name="state">The existing or old state of the dialog.</param>
    /// <returns>The updated or new state of the dialog.</returns>
    private static async Task<JObject> RunTurnAsync(Dialog dialog, ITurnContext turnContext, JObject state, CancellationToken cancellationToken)
    {
        // If we have some state, deserialize it. (This mimics the shape produced by BotState.cs.)
        var dialogStateProperty = state?[nameof(DialogState)];
        var dialogState = dialogStateProperty?.ToObject<DialogState>(StateJsonSerializer);

        // A custom accessor is used to pass a handle on the state to the dialog system.
        var accessor = new RefAccessor<DialogState>(dialogState);

        // Run the dialog.
        await dialog.RunAsync(turnContext, accessor, cancellationToken);

        // Serialize the result (available as Value on the accessor), and put its value back into a new JObject.
        return new JObject { { nameof(DialogState), JObject.FromObject(accessor.Value, StateJsonSerializer) } };
    }
}

Für den benutzerdefinierten Accessor müssen wir nur „Get“ implementieren, weil auf den Zustand verwiesen wird:And finally, the custom Accessor, we only need to implement Get because the state is by ref:

RefAccessor.csRefAccessor.cs

public class RefAccessor<T> : IStatePropertyAccessor<T>
    where T : class
{
    public RefAccessor(T value)
    {
        Value = value;
    }

    public T Value { get; private set; }

    public string Name => nameof(T);

    public Task<T> GetAsync(ITurnContext turnContext, Func<T> defaultValueFactory = null, CancellationToken cancellationToken = default(CancellationToken))
    {
        if (Value == null)
        {
            if (defaultValueFactory == null)
            {
                throw new KeyNotFoundException();
            }

            Value = defaultValueFactory();
        }

        return Task.FromResult(Value);
    }

    #region Not Implemented
    public Task DeleteAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
    {
        throw new NotImplementedException();
    }

    public Task SetAsync(ITurnContext turnContext, T value, CancellationToken cancellationToken = default(CancellationToken))
    {
        throw new NotImplementedException();
    }
    #endregion
}

Zusätzliche InformationenAdditional information

Der in diesem Artikel verwendete C#-Beispielcode ist auf GitHub verfügbar.The C# sample code used in this article is available on GitHub.