Implementación de almacenamiento personalizado en un botImplement custom storage for your bot

se aplica a: SDK V4APPLIES TO: SDK v4

Las interacciones de un bot se dividen en tres áreas: en primer lugar, el intercambio de actividades con Azure Bot Service, en segundo lugar, la carga y almacenamiento de estados de diálogos con Store y, por último, cualquier otro servicio back-end que el bot necesite para realizar su trabajo.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.

diagrama de interacción de ampliación

Requisitos previosPrerequisites

  • El código de ejemplo completo que se usa en este artículo puede encontrarse aquí: Ejemplo en C#.The full sample code used in this article can be found here: C# sample.

En este artículo, vamos a analizar la semántica en torno a las interacciones del bot con Azure Bot Service y con Store.In this article, we will be exploring the semantics around the bot's interactions with the Azure Bot Service and the Store.

Bot Framework incluye una implementación predeterminada. Muy probablemente, esta implementación se adaptará a las necesidades de muchas aplicaciones y todo lo que tendrá que hacer para usarla es conectar todas las piezas con unas pocas líneas de código de inicialización.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. Muchos de los ejemplos muestran exactamente eso.Many of the samples illustrate just that.

El objetivo en este caso, sin embargo, es describir lo que puede hacer cuando la semántica de la implementación predeterminada no funciona como le gustaría en la aplicación.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. Lo principal es que esta es una plataforma y no una aplicación rígida con un comportamiento fijo. En otras palabras, la implementación de muchos de los mecanismos de la plataforma es solo la implementación predeterminada, pero no la única implementación posible.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.

Más concretamente, la plataforma no dicta la relación entre el intercambio de actividades con Azure Bot Service y la carga y almacenamiento de cualquier estado del bot, solo proporciona una opción predeterminada.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. Para ilustrar este punto aún más, vamos a desarrollar una implementación alternativa con una semántica diferente.To illustrate this point further, we will be developing an alternative implementation that has different semantics. Esta solución alternativa se adapta igualmente bien a la plataforma y puede incluso ser más adecuada para la aplicación que se está desarrollando.This alternative solution sits equally well in the framework and may even be more appropriate for the application being developed. Todo depende del escenario.It all depends on the scenario.

Comportamiento de los proveedores predeterminados de BotFrameworkAdapter y StorageBehavior of the default BotFrameworkAdapter and Storage providers

En primer lugar, vamos a revisar la implementación predeterminada que se incluye como parte de los paquetes de la plataforma tal y como se muestra en el siguiente diagrama de secuencia:Firstly, let's review the default implementation that ships as part of the framework packages as shown by the following sequence diagram:

diagrama predeterminado de ampliación

Al recibir una actividad, el bot carga el estado correspondiente a esta conversación.On receiving an Activity, the bot loads the state corresponding to this conversation. A continuación, ejecuta la lógica del diálogo con ese estado y la actividad que acaba de llegar.It then runs the dialog logic with this state and the Activity that has just arrived. Durante la ejecución del diálogo, se crearán una o varias actividades salientes y se enviarán inmediatamente.In the process of executing the dialog, one or more outbound activities are created and immediately sent. Una vez que el procesamiento del diálogo está completo, el bot guarda el estado actualizado y sobrescribe el anterior estado con el nuevo.When the processing of the dialog is complete, the bot saves the updated state, overwriting the old state with new.

Merece la pena tener en cuenta un par de cosas que pueden salir mal con este comportamiento.It is worth considering a couple of things that can go wrong with this behavior.

En primer lugar, si se ha producido un error en la operación de guardar por algún motivo, el estado pierde la sincronización con lo que se ve en el canal. El usuario que vea las respuestas tendrá la impresión de que el estado ha cambiado, pero realmente no es así.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. Esto resulta, por lo general, peor que si el estado fue correcto al igual que los mensajes de respuesta.This is generally worse than if the state was successful and the response messaging were successful. Esto puede tener implicaciones en el diseño de la conversación: por ejemplo, el diálogo podría incluir intercambios de confirmación adicionales y redundantes con el usuario.This can have implications for the conversation design: for example, the dialog might include additional, otherwise redundant confirmation exchanges with the user.

En segundo lugar, si la implementación se escala horizontalmente entre varios nodos, puede que el estado se sobrescriba accidentalmente, lo cual puede resultar especialmente confuso ya que el diálogo habrá enviado probablemente actividades al canal que incluyen mensajes de confirmación.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. Considere el ejemplo de un bot para pedir pizza. Si el usuario, cuando se le pregunta por un ingrediente agrega champiñones e inmediatamente después agrega queso, en un escenario de escalabilidad horizontal con varias instancias ejecutando actividades subyacentes, este se puede enviar simultáneamente a las distintas máquinas que ejecutan el bot.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. Cuando esto sucede, se produce lo que se conoce como "condición de carrera" por la cual una máquina podría sobrescribir el estado que ha escrito otra.When this happens, there is what is referred to as a "race condition" where one machine might overwrite the state written by another. Sin embargo, en nuestro escenario, dado que ya se han enviado las respuestas, el usuario ha recibido confirmación de que se agregaron los champiñones y el queso.However, in our scenario, because the responses were already sent, the user has received confirmation that both mushroom and cheese were added. Lamentablemente, cuando llega la pizza, solo llevará champiñones o queso, pero no ambos.Unfortunately, when the pizza arrives, it will only contain mushroom or cheese, not both.

Bloqueo optimistaOptimistic locking

La solución consiste en introducir algún bloqueo para proteger el estado.The solution is to introduce some locking around the state. El estilo concreto de bloqueo que se va a emplear aquí se llama bloqueo optimista ya que va a dejar que todo se ejecute como si fuera el único proceso en ejecución y, posteriormente, se detectarán todas las infracciones de simultaneidad una vez que el proceso se haya completado.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. Esto puede parecer complicado pero es muy fácil de compilar mediante tecnologías de almacenamiento en la nube y los puntos de extensión adecuados en la plataforma del bot.This may sound complicated but is very easy to build using cloud storage technologies and the right extension points in the bot framework.

Vamos a usar un mecanismo estándar de HTTP basado en el encabezado de etiqueta de entidad (ETag).We will use a standard HTTP mechanism based on the entity tag header, (ETag). Entender este mecanismo resulta crucial para comprender el código que sigue.Understanding this mechanism is crucial to understanding the code that follows. En el siguiente diagrama se ilustra la secuencia.The following diagram illustrates the sequence.

diagrama de error de condición previa de ampliación

El diagrama ilustra el caso de dos clientes que están llevando a cabo una actualización en algún recurso.The diagram illustrates the case of two clients that are performing an update to some resource. Cuando un cliente emite una solicitud GET y se devuelve un recurso desde el servidor, este viene acompañado de un encabezado ETag.When a client issues a GET request and a resource is returned from the server, it is accompanied by an ETag header. El encabezado ETag es un valor opaco que representa el estado del recurso.The ETag header is an opaque value that represents the state of the resource. Si se cambia un recurso, se actualizará el encabezado ETag.If a resource is changed, the ETag will be updated. Una vez que el cliente haya hecho la actualización del estado, se vuelve a publicar en el servidor. Al hacer esta solicitud, el cliente asocia el valor ETag que ha recibido anteriormente a un encabezado If-Match de condición previa.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. Si este valor de ETag no coincide con el último valor que devolvió el servidor (en cualquier respuesta, para cualquier cliente), se producirá un error 412 de condición previa en la comprobación.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. Este error es un indicador para el cliente que realiza la solicitud POST de que el recurso se ha actualizado.This failure is an indicator to the client making the POST request that the resource has been updated. Al ver este error, el comportamiento normal de un cliente será volver a obtener el recurso, aplicar la actualización que deseaba y publicar el recurso de nuevo.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. Esta segunda solicitud POST será correcta suponiendo, naturalmente, que ningún otro cliente haya llegado y actualizado el recurso y, en ese caso, el cliente solo tendría que intentarlo de nuevo.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.

Este proceso se denomina "optimista" porque el cliente, una vez obtenido un recurso, pasa a realizar su procesamiento porque el recurso en sí no está "bloqueado" en el sentido de que otros usuarios pueden acceder a él sin ninguna restricción.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. Cualquier contención entre los clientes sobre cuál debe ser el estado del recurso no se determina hasta que se ha realizado el procesamiento.Any contention between clients over what the state of the resource should be is not determined until the processing has been done. Como norma, en un sistema distribuido, esta estrategia es mejor que el enfoque opuesto, el "pesimista".As a rule, in a distributed system this strategy is more optimal than the opposite "pessimistic" approach.

El mecanismo de bloqueo optimista que hemos explicado aquí supone que la lógica del programa se puede volver a intentar con seguridad. No es necesario decir que lo importante aquí es considerar qué sucede a las llamadas de servicio externas.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. En este caso, la solución ideal es que se pueda hacer que estos servicios sean idempotentes.The ideal solution here is if these services can be made idempotent. En informática, una operación idempotente es aquella que no tiene ningún efecto adicional si se la llama varias veces con los mismos parámetros de entrada.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. Los servicios HTTP REST puros que implementan las operaciones GET, PUT y DELETE se ajustan a esta descripción.Pure HTTP REST services that implement GET, PUT and DELETE fit this description. El razonamiento aquí es intuitivo: podríamos volver a intentar el procesamiento y el hacer todas las llamadas necesarias no tendría ningún efecto adicional ya que ellas se vuelven a ejecutar como parte de ese reintento.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. Por lo que respecta a este análisis, vamos a suponer que se trata de un escenario ideal y que los servicios de back-end que aparecen a la derecha de la imagen del sistema al principio de este artículo son todos servicios HTTP REST idempotentes. A partir de aquí solo nos centraremos en el intercambio de actividades.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.

Almacenamiento en búfer de actividades salientesBuffering outbound activities

El envío de una actividad no es una operación idempotente, ni está claro que tenga mucho sentido en un escenario de un extremo a otro.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. Después de todo, la actividad a menudo solo transmite un mensaje que se anexa a una vista o que quizás ha pronunciado un agente de texto a voz.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.

Lo más importante que queremos evitar en el envío de actividades es enviarlas varias veces.The key thing we want to avoid with sending the activities is sending them multiple times. El problema es que el mecanismo de bloqueo optimista requerirá posiblemente que se vuelva a ejecutar nuestra lógica varias veces.The problem we have is that the optimistic locking mechanism requires that we with rerun our logic possibly multiple times. La solución es sencilla: debemos almacenar en búfer las actividades salientes del diálogo hasta que estemos seguros de que no vamos a volver a ejecutar la lógica.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. Y eso solo se producirá cuando hayamos completado una operación de guardar correctamente.That is until after we have a successful Save operation. Buscamos un flujo parecido al siguiente:We are looking for a flow that looks something like the following:

diagrama de búfer de ampliación

Suponiendo que podamos crear un bucle de reintento en torno a la ejecución del diálogo, obtendremos el siguiente comportamiento si se produce un error de condición previa en la operación de guardar: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:

Ampliación guardar diagrama

Si aplicamos este mecanismo y repasamos el ejemplo desde el principio no veremos nunca una confirmación positiva errónea de un ingrediente de pizza agregado a un pedido.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. De hecho, aunque podríamos haber escalado horizontalmente nuestra implementación en varias máquinas, hemos serializado eficazmente las actualizaciones de estado con el esquema de bloqueo optimista.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. En el ejemplo de pedido de pizza, la confirmación después de agregar un elemento se puede escribir ahora para reflejar el estado completo correctamente.In our pizza ordering but the acknowledgement from adding an item can now even be written to reflect the full state accurately. Por ejemplo, si el usuario escribe inmediatamente "queso" y, a continuación, antes de que el bot haya tenido ocasión de responder "champiñones", las dos respuestas ahora pueden ser "pizza con queso" y, después, "pizza con queso y champiñones".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."

Si observamos el diagrama de secuencia, podemos ver que las respuestas se podrían perder después de una operación de guardar correcta. No obstante, también se podrían perder en cualquier punto de todo el proceso de comunicación.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. La cuestión en este caso es que no se trata de un problema que la infraestructura de administración de estados pueda solucionar.The point is this is not a problem the state management infrastructure can fix. Requerirá un protocolo de nivel superior y, posiblemente, uno que incluya al usuario del canal.It will require a higher-level protocol and possibly one involving the user of the channel. Por ejemplo, si el usuario considera que el bot no ha respondido, es razonable esperar que el usuario lo volverá intentar de nuevo o esperar un comportamiento similar.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. Por tanto, aunque podría ser razonable que un escenario como este tenga interrupciones transitorias, es mucho menos razonable esperar que un usuario pueda filtrar confirmaciones positivas erróneas o cualquier otro tipo de mensajes no intencionados.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.

En resumen, en nuestra nueva solución de almacenamiento personalizada, vamos a hacer tres cosas que la implementación personalizada de la plataforma no puede hacer.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. En primer lugar, vamos a usar ETags para detectar la contención. En segundo lugar, vamos a volver a intentar el procesamiento si se detecta un error de ETag y, por último, vamos a almacenar en búfer todas las actividades salientes hasta que se produzca una operación de guardar correcta.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. El resto de este artículo describe la implementación de estas tres partes.The remainder of this article describes the implementation of these three parts.

Implementación de la compatibilidad con ETagImplementing ETag Support

Comenzaremos definiendo una interfaz para nuestra nueva tienda con compatibilidad con ETag.We start out by defining an interface for our new store with ETag support. La interfaz hará que sea mucho más fácil aprovechar los mecanismos de inyección de dependencia de que disponemos en ASP.NET.The interface will make it very easy to leverage the dependency injection mechanisms we have in ASP.NET. Tener la interfaz significa que podemos implementar una versión para producción.Having the interface means we can implement a version for production. También se puede implementar una versión para las pruebas unitarias que se ejecutan en memoria sin necesidad de llegar a la red.We could also implement a version for unit tests that runs in memory without the need of hitting the network.

La interfaz consta de los métodos Load y Save.The interface consists of Load and Save methods. Ambos emplean la clave que usaremos para el estado.Both these take the key we will use for the state. El método Load devolverá los datos y la ETag asociada.The Load will return the data and the associated ETag. Y el método Save los guardará.And the Save will take these in. Además, el método Save devolverá un valor booleano.Additionally, the Save will return bool. Este valor booleano indicará si la ETag coincide y si el método Save se realizó correctamente.This bool will indicate whether the ETag has matched and the Save was successful. No se pretende usar esto como un indicador general de error si no, más bien, como un indicador específico de error de condición previa. Vamos a modelar esto como un código de devolución en lugar de como una excepción ya que escribiremos la lógica del flujo de control con la forma de nuestro bucle de reintentos.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.

Como nos gustaría que el nivel inferior de este elemento de almacenamiento fuera conectable, nos aseguraremos de evitar incluir requisitos de serialización en él. No obstante, nos gustaría especificar que el contenido guardado debe ser JSON ya que, de esa forma, el tipo de contenido puede establecerse durante la implementación del almacén.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. La manera más fácil y natural de hacer esto en .NET es a través de tipos de argumento y, más concretamente, vamos a escribir el argumento del contenido como 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. En JavaScript o TypeScript esto será un objeto nativo normal.In JavaScript or TypeScript this will just be a regular native object.

Esta es la interfaz resultante: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);
}

La implementación de esto en Azure Blob Storage es sencilla.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;
    }
}

Como puede verse aquí, Azure Blob Storage es el que realiza el trabajo aquí.As you can see Azure Blob Storage is doing the real work here. Tenga en cuenta la captura de excepciones específicas y cómo que se traduce esto a la hora de cumplir con las expectativas del código que llama.Note the catch of specific exceptions and how that is translated across to meet what will be the expectations of the calling code. Es decir, queremos que, en la carga, una excepción No encontrado devuelva un valor NULL y que la excepción de guardado Error de condición previa devuelva un valor booleano.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.

Todo este código fuente estará disponible en el ejemplo correspondiente y ese ejemplo incluirá una implementación de almacén de memoria.All this source code will be available in a corresponding sample and that sample will include a memory store implementation.

Implementación del bucle de reintentosImplementing the Retry Loop

La forma básica del bucle se deriva directamente del comportamiento que se muestra en los diagramas de secuencia.The basic shape of the loop is derived directly from the behavior shown in the sequence diagrams.

Al recibir una actividad se crea una clave para el estado correspondiente de esa conversación.On receiving an Activity we create a key for the corresponding state for that conversation. No se va a cambiar la relación entre la actividad y el estado de la conversación, por lo que se va a crear la clave exactamente de la misma forma que se hizo en la implementación de estado predeterminada.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.

Después de haber creado la clave adecuada se intentará cargar el estado correspondiente.After having created the appropriate key we will attempt to Load the corresponding state. Posteriormente, se ejecutarán los diálogos del bot y, finalmente, se intentará realizar la operación de guardar.Then run the bot's dialogs and then attempt to Save. Si la operación de guardar es correcta, se enviarán las actividades salientes que resultaron de la ejecución del diálogo y terminaremos.If that Save is successful, we will send the outbound Activities that resulted from running the dialog and be done. En caso contrario, volveremos a repetir todo el proceso desde antes de la carga.Otherwise we will go back and repeat the whole process from before the Load. Rehacer la carga nos ofrecerá un nuevo valor de ETag de modo que la próxima vez, la operación de guardar se realizará correctamente.Redoing the Load will give us a new ETag and so next time the Save will hopefully be successful.

La implementación OnTurn resultante tiene el siguiente aspecto: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;
        }
    }
}

Tenga en cuenta que hemos modelado la ejecución del diálogo como una llamada de función.Note that we have modeled the dialog execution as a function call. Quizás, en una implementación más sofisticada, se habría definido una interfaz y hecho que esta dependencia fuera inyectable, pero para nuestros propósitos, hacer que el diálogo esté detrás de una función estática enfatiza la naturaleza funcional de nuestro enfoque.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. Como norma general, organizar la implementación de tal forma que los elementos cruciales sean funcionales nos sitúa en muy buena posición en lo que respecta al uso correcto en las redes.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.

Implementación del almacenamiento en búfer para actividades salientesImplementing outbound Activity buffering

El siguiente requisito es que las actividades salientes se almacenen en búfer hasta que se haya realizado una operación de guardar correcta.The next requirement is that we buffer outbound Activities until a successful Save has been performed. Esto requerirá una implementación personalizada de BotAdapter.This will require a custom BotAdapter implementation. En este código, se implementará la función abstracta SendActivity para agregar la actividad a una lista en lugar de enviarla.In this code, we will implement the abstract SendActivity function to add the Activity to a list rather than sending it. El diálogo que vamos a hospedar no será el más correcto.The dialog we will be hosting will be non-the-wiser. En este escenario en particular, no se admiten las operaciones UpdateActivity y DeleteActivity, por lo que estos métodos solo generarán respuestas de No implementado.In this particular scenario UpdateActivity and DeleteActivity operations are not supported and so will just throw Not Implemented from those methods. Tampoco nos vamos a preocupar por el valor devuelto por la operación SendActivity.We also don't care about the return value from the SendActivity. Esto lo usan algunos canales en casos en los que se deben enviar las actualizaciones de las actividades, por ejemplo, para deshabilitar los botones o tarjetas que aparecen en el canal.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. Estos intercambios de mensajes pueden resultar complicados, especialmente cuando se requiere el estado, lo cual está fuera del objetivo de este artículo.These message exchanges can get complicated particularly when state is required, that is outside the scope of this article. La implementación completa del BotAdapter personalizado tiene este aspecto: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
}

IntegraciónIntegration

Ya solo queda unir todas estas diversas piezas y conectarlas a las piezas existentes en el marco.All that is left to do is glue these various new pieces together and plug them into the existing framework pieces. El bucle de reintentos principal se encuentra en la función IBot OnTurn.The main retry loop just sits in the IBot OnTurn function. Contiene nuestra implementación IStore personalizada que, con fines de prueba, hemos hecho que sea con dependencia inyectable.It holds our custom IStore implementation which for testing purposes we have made dependency injectable. Hemos puesto todo el código de hospedaje del diálogo en una clase denominada DialogHost que expone una única función estática pública.We have put all the dialog hosting code into a class called DialogHost that exposes a single public static function. Esta función está definida para tomar la actividad entrante y el estado anterior y, a continuación, devolver las actividades resultantes y el nuevo estado.This function is defined to take the inbound Activity and the old state and then return the resulting Activities and new state.

Lo primero que hay que hacer en esta función es crear el BotAdapter personalizado que se mencionó anteriormente.The first thing to do in this function is to create the custom BotAdapter we introduced earlier. Posteriormente, vamos a ejecutar el diálogo exactamente de la misma forma en que lo solemos hacer creando un DialogSet y un DialogContext, y realizando los flujos Continue o Begin habituales.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. La única parte que no hemos tratado es la necesidad de un descriptor de acceso personalizado.The only piece we haven't covered is the need for a custom Accessor. Esto resulta ser una clase shim muy sencilla que facilita el traslado del estado del diálogo al sistema de diálogos.This turns out to be a very simple shim that facilitates passing the dialog state into the dialog system. El descriptor de acceso utiliza semántica de referencia cuando trabaja con el sistema de diálogos y, por ello, lo único necesario es pasar el control.The Accessor uses ref semantics when working with the dialog system and so all that is needed is to pass the handle across. Para facilitar aún más las cosas, hemos restringido la plantilla de clase que vamos a usar para la semántica de referencia.To make things even clearer we have constrained the class template we are using to ref semantics.

Estamos siendo muy cuidadosos con la disposición en capas, y vamos a poner el código de JsonSerialization insertado en el código de hospedaje porque no queremos que esté dentro de la capa de almacenamiento conectable, ya que las diferentes implementaciones se podrían serializar de distinta manera.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.

Este es el código del controlador: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) } };
    }
}

Y por último, el descriptor de acceso personalizado, en el que solo debemos implementar Get, porque el estado es por referencia: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
}

Información adicionalAdditional information

El código de ejemplo de C# que se usa en este artículo está disponible en GitHub.The C# sample code used in this article is available on GitHub.