Exportar (0) Imprimir
Expandir todo

Integrar un servicio de flujo de trabajo WCF con temas y colas de Service Bus

Actualizado: marzo de 2015

Autor: Paolo Salvatori

Revisores: Ralph Squillace, Sidney Higa

Este documento proporciona orientación sobre cómo integrar un servicio de flujo de trabajo WCF con colas y temas de Microsoft Azure Service Bus. El documento le guía a través del modo de crear un servicio de flujo de trabajo WCF que utilice las colas y los temas para intercambiar mensajes con una aplicación cliente. Como complemento de esta guía, consulte el artículo Uso de la mensajería de Bus de servicio de Azure de Roman Kiss, que proporciona una solución más sofisticada para el mismo problema y un conjunto de actividades reutilizables que puede usar en un servicio de flujo de trabajo WCF para interactuar con las colas y los temas de CmdLets.

Para obtener más información sobre Microsoft Azure Service Bus, consulte los siguientes recursos:

La ilustración siguiente muestra la arquitectura de alto nivel de la demostración:

Temas-de-colas-del-bus-de-servicio-Servicio-de-flujo-de-trabajo-de-WCF4

  1. Una aplicación cliente utiliza un proxy WCF y NetMessagingBinding para enviar un mensaje de solicitud a requestqueue o a requesttopic.

  2. El servicio del flujo de trabajo WCF que se ejecuta en una aplicación de consola o IIS recibe el mensaje de solicitud de la suscripción requestqueue o ItalyMilan definida en requesttopic.

    Temas-de-colas-del-bus-de-servicio-Servicio-de-flujo-de-trabajo-de-WCF5
  3. El servicio del flujo de trabajo WCF, que se muestra en la ilustración anterior, realiza las siguientes acciones:

    • La BrokeredMessagePropertyActivity personalizada (identificada mediante el nombre para mostrar Get BrokeredMessage) lee BrokeredMessageProperty del mensaje entrante y asigna su valor a una variable de flujo de trabajo definida fuera de la actividad Sequential.

    • La actividad Receive recupera el mensaje de la suscripción requestqueue o ItalyMilan de requesttopic.

    • El CalculatorActivity personalizado recibe el mensaje entrante y BrokeredMessageProperty como argumentos de entrada, procesa el mensaje de solicitud y genera un mensaje de respuesta y un BrokeredMessageProperty saliente. Al ejecutarse en una aplicación de consola, la actividad realiza el seguimiento de las propiedades de las BrokeredMessageProperty entrante y saliente en la salida estándar.

    • La actividad If lee la dirección contenida en la propiedad ReplyTo de la BrokeredMessageProperty entrante.

      • Si la cadena contiene la palabra “tema”, la respuesta se envía a responsetopic.

      • Si no, la respuesta se envía a responsequeue.

    • En ambos casos, una instancia de BrokeredMessagePropertyActivity (identificada mediante el nombre para mostrar Set BrokeredMessage) se utiliza para ajustar actividad Send y asignar la BrokeredMessageProperty saliente a la colección de propiedades del mensajes de respuesta WCF.

  4. El servicio del flujo de trabajo WCF escribe el mensaje de respuesta a responsequeue o a responsetopic.

  5. La aplicación cliente utiliza un servicio WCF con dos extremos diferentes para recuperar el mensaje de respuesta de responsequeue o de responsetopic. En un entorno con varias aplicaciones cliente, cada una debe utilizar una cola o una suscripción independiente para recibir los mensajes de respuesta de BizTalk. Hay más información sobre esto más adelante en el artículo.

Ahora que hemos descrito la arquitectura global de la demostración, podemos analizar detalladamente los componentes principales de la solución.

La primera operación que hay que realizar para configurar correctamente el entorno es crear las entidades de mensajería que utiliza la demostración. La primera operación que se va a ejecutar es provisionar un nuevo espacio de nombres de CmdLets o modificar un espacio de nombres existente para incluir CmdLets. Puede realizar esta tarea desde el Portal de administración de Azure respectivamente haciendo clic en el botón Nuevo o Modificar.

El paso siguiente consiste en crear las entidades de la cola, el tema y la suscripción que requiera la demostración. Como mencioné en la introducción, tiene muchas opciones para realizar esta tarea. La manera más fácil es usar el Portal de administración de Azure con los botones del comando Administrar entidades.

Puede usar la vista de árbol de navegación mostrada en el punto 2 para seleccionar una entidad existente y mostrar sus propiedades en la barra vertical resaltada en el punto 3. Para quitar la entidad seleccionada, presione el botón Eliminar en la barra de comandos Administrar entidades.

noteNota
Usar el Portal de administración de Azure es una forma práctica y adecuada de controlar las entidades de mensajería en un espacio de nombres de CmdLets determinado. Sin embargo, al menos por el momento, el conjunto de operaciones que un desarrollador o un administrador del sistema pueden realizar con la interfaz de usuario es muy limitado. Por ejemplo, el portal de administración de Azure realmente permite que un usuario cree las colas, los temas y las suscripciones y defina sus propiedades, pero no le permite crear ni mostrar las reglas de una suscripción existente. Ahora, solo puede realizar esta tarea mediante la API Mensajería .NET. Concretamente, para agregar una nueva regla a una suscripción existente, puede utilizar los métodos AddRule(cadena, filtro) o AddRule(RuleDescription) expuestos por la clase SubscriptionClient, mientras que para enumerar las reglas de una suscripción existente, puede utilizar el método GetRules de la clase NamespaceManager. Además, el portal de administración de Azure en realidad no proporciona la capacidad de realizar las operaciones siguientes:

  1. Visualizar correctamente las entidades de una forma jerárquica. De hecho, el portal de administración de Azure muestra las colas, los temas y las suscripciones con una vista de árbol plana. Sin embargo, puede organizar las entidades de mensajería en una estructura jerárquica especificando simplemente su nombre como ruta de acceso absoluta compuesta por varios segmentos de ruta, por ejemplo crm/prod/queues/orderqueue.

  2. Exporte las entidades de mensajería contenidas en un espacio de nombres de CmdLets determinado a un archivo de enlace XML (de BizTalk Server). En su lugar, la herramienta Explorador de CmdLets proporciona la capacidad de seleccionar y exportar

    1. Entidades individuales.

    2. Entidades por tipo (colas o temas).

    3. Entidades contenidas en cierta ruta de acceso (por ejemplo crm/prod/queues).

    4. Todas las entidades de un espacio de nombres especificado.

  3. Importar las colas, los temas y las suscripciones de un archivo XML a un espacio de nombres existente. El Explorador de CmdLets admite la capacidad de exportar entidades a un archivo XML y de reimportarlas en el mismo o en otro espacio de nombres de CmdLets. Esta característica viene bien para realizar la copia de seguridad y la restauración de un espacio de nombres o simplemente para transferir las colas y los temas de una prueba a un espacio de nombres de producción.

El Explorador de Service Bus permite a un usuario crear, eliminar y probar las colas, los temas, las suscripciones y las reglas y representa el complemento perfecto del portal de administración de Azure oficial.

Para su comodidad, creé una aplicación de consola denominada Aprovisionamiento que utiliza la funcionalidad proporcionada por la clase NamespaceManager para crear las colas, los temas y las suscripciones que requiere la solución. Cuando se inicia, las aplicaciones de consola piden las credenciales del espacio de nombres de servicio. Se utilizan para autenticar al servicio de control de acceso y adquirir un token de acceso que demuestre a la infraestructura de CmdLets que la aplicación está autorizada a proporcionar las nuevas entidades de mensajería. A continuación, la aplicación pide el valor que se va a asignar a las propiedades de las entidades que se van a crear, como EnabledBatchedOperations y EnableDeadLetteringOnMessageExpiration para las colas. La aplicación de aprovisionamiento crea las entidades siguientes en el espacio de nombres de CmdLets especificado:

  1. Una cola denominada requestqueue que la aplicación cliente usa para enviar mensajes de solicitud a BizTalk Server.

  2. Una cola denominada responsequeue que BizTalk Server usa para enviar mensajes de respuesta a la aplicación cliente.

  3. Un tema denominado requesttopic que la aplicación cliente usa para enviar mensajes de solicitud a BizTalk Server.

  4. Un tema denominado responsetopic que BizTalk Server usa para enviar mensajes de respuesta a la aplicación cliente.

  5. Una suscripción denominada ItalyMilan para requesttopic. BizTalk Server utiliza esta última suscripción para recibir los mensajes de requesttopic. La suscripción en cuestión tiene una sola regla definida como sigue:

    1. Filtro: Country='Italy' and City='Milan'

    2. Acción: Set Area='Western Europe'

  6. Una suscripción denominada ItalyMilan para responsetopic. La última la utiliza la aplicación cliente para recibir los mensajes de respuesta de responsetopic. La suscripción en cuestión tiene una sola regla definida como sigue:

    1. Filtro: Country='Italy' and City='Milan'

    2. Acción: Set Area='Western Europe'

La ilustración siguiente muestra la salida de la aplicación de consola Provisioning.

Temas-de-colas-del-bus-de-servicio-Servicio-de-flujo-de-trabajo-de-WCF8

La siguiente tabla contiene el código de la aplicación Provisioning:

#region Using Directives
using System;
using Microsoft.ServiceBus;
using Microsoft.ServiceBus.Messaging;
#endregion

namespace Microsoft.WindowsAzure.CAT.Samples.ServiceBus.Provisioning
{
    static class Program
    {
        #region Private Constants
        //***************************
        // Constants
        //***************************

        private const string RequestQueue = "requestqueue";
        private const string ResponseQueue = "responsequeue";
        private const string RequestTopic = "requesttopic";
        private const string ResponseTopic = "responsetopic";
        private const string RequestSubscription = "ItalyMilan";
        private const string ResponseSubscription = "ItalyMilan";
        #endregion
        static void Main()
        {
            var defaultColor = Console.ForegroundColor;

            try
            {
                // Set Window Size
                Console.WindowWidth = 100;
                Console.BufferWidth = 100;
                Console.WindowHeight = 48;
                Console.BufferHeight = 48;

                // Print Header            
                Console.WriteLine("Read Credentials:");
                Console.WriteLine("-----------------");

                // Read Service Bus Namespace
                Console.ForegroundColor = ConsoleColor.Green;
                Console.Write("Service Bus Namespace: ");
                Console.ForegroundColor = defaultColor;
                var serviceNamespace = Console.ReadLine();

                // Read Service Bus Issuer Name
                Console.ForegroundColor = ConsoleColor.Green;
                Console.Write("Service Bus Issuer Name: ");
                Console.ForegroundColor = defaultColor;
                var issuerName = “RootManageSharedAccessKey”;

                // Read Service Bus Issuer Secret
                Console.ForegroundColor = ConsoleColor.Green;
                Console.Write("Service Bus Issuer Secret: ");
                Console.ForegroundColor = defaultColor;
                var SASKey = Console.ReadLine();

                // Print Header
                Console.WriteLine();
                Console.WriteLine("Enter Queues Properties:");
                Console.WriteLine("------------------------");

                // Read Queue EnabledBatchedOperations
                Console.ForegroundColor = ConsoleColor.Green;
                Console.Write("Queues: ");
                Console.ForegroundColor = defaultColor;
                Console.Write("Set the ");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write("EnabledBatchedOperations ");
                Console.ForegroundColor = defaultColor;
                Console.Write("to true [y=Yes, n=No]?");
                var key = Console.ReadKey().KeyChar;
                var queueEnabledBatchedOperations = key == 'y' || key == 'Y';
                Console.WriteLine();

                // Read Queue EnableDeadLetteringOnMessageExpiration
                Console.ForegroundColor = ConsoleColor.Green;
                Console.Write("Queues: ");
                Console.ForegroundColor = defaultColor;
                Console.Write("Set the ");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write("EnableDeadLetteringOnMessageExpiration ");
                Console.ForegroundColor = defaultColor;
                Console.Write("to true [y=Yes, n=No]?");
                key = Console.ReadKey().KeyChar;
                var queueEnableDeadLetteringOnMessageExpiration = key == 'y' || key == 'Y';
                Console.WriteLine();

                // Read Queue RequiresDuplicateDetection
                Console.ForegroundColor = ConsoleColor.Green;
                Console.Write("Queues: ");
                Console.ForegroundColor = defaultColor;
                Console.Write("Set the ");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write("RequiresDuplicateDetection ");
                Console.ForegroundColor = defaultColor;
                Console.Write("to true [y=Yes, n=No]?");
                key = Console.ReadKey().KeyChar;
                var queueRequiresDuplicateDetection = key == 'y' || key == 'Y';
                Console.WriteLine();

                // Read Queue RequiresSession
                Console.ForegroundColor = ConsoleColor.Green;
                Console.Write("Queues: ");
                Console.ForegroundColor = defaultColor;
                Console.Write("Set the ");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write("RequiresSession ");
                Console.ForegroundColor = defaultColor;
                Console.Write("to true [y=Yes, n=No]?");
                key = Console.ReadKey().KeyChar;
                var queueRequiresSession = key == 'y' || key == 'Y';
                Console.WriteLine();

                // Print Header
                Console.WriteLine();
                Console.WriteLine("Enter Topic Properties:");
                Console.WriteLine("-----------------------");

                // Read Topic EnabledBatchedOperations
                Console.ForegroundColor = ConsoleColor.Green;
                Console.Write("Topics: ");
                Console.ForegroundColor = defaultColor;
                Console.Write("Set the ");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write("EnabledBatchedOperations ");
                Console.ForegroundColor = defaultColor;
                Console.Write("to true [y=Yes, n=No]?");
                key = Console.ReadKey().KeyChar;
                var topicEnabledBatchedOperations = key == 'y' || key == 'Y';
                Console.WriteLine();

                // Read Topic RequiresDuplicateDetection
                Console.ForegroundColor = ConsoleColor.Green;
                Console.Write("Topics: ");
                Console.ForegroundColor = defaultColor;
                Console.Write("Set the ");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write("RequiresDuplicateDetection ");
                Console.ForegroundColor = defaultColor;
                Console.Write("to true [y=Yes, n=No]?");
                key = Console.ReadKey().KeyChar;
                var topicRequiresDuplicateDetection = key == 'y' || key == 'Y';
                Console.WriteLine();

                // Print Header
                Console.WriteLine();
                Console.WriteLine("Enter Subscriptions Properties: ");
                Console.WriteLine("-------------------------------");

                // Read Subscription EnabledBatchedOperations
                Console.ForegroundColor = ConsoleColor.Green;
                Console.Write("Subscriptions: ");
                Console.ForegroundColor = defaultColor;
                Console.Write("Set the ");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write("EnabledBatchedOperations ");
                Console.ForegroundColor = defaultColor;
                Console.Write("to true [y=Yes, n=No]?");
                key = Console.ReadKey().KeyChar;
                var subscriptionnabledBatchedOperations = key == 'y' || key == 'Y';
                Console.WriteLine();

                // Read Subscription EnableDeadLetteringOnFilterEvaluationExceptions
                Console.ForegroundColor = ConsoleColor.Green;
                Console.Write("Subscriptions: ");
                Console.ForegroundColor = defaultColor;
                Console.Write("Set the ");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write("EnableDeadLetteringOnFilterEvaluationExceptions ");
                Console.ForegroundColor = defaultColor;
                Console.Write("to true [y=Yes, n=No]?");
                key = Console.ReadKey().KeyChar;
                var subscriptionEnableDeadLetteringOnFilterEvaluationExceptions = key == 'y' || key == 'Y';
                Console.WriteLine();

                // Read Subscription EnableDeadLetteringOnMessageExpiration
                Console.ForegroundColor = ConsoleColor.Green;
                Console.Write("Subscriptions: ");
                Console.ForegroundColor = defaultColor;
                Console.Write("Set the ");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write("EnableDeadLetteringOnMessageExpiration ");
                Console.ForegroundColor = defaultColor;
                Console.Write("to true [y=Yes, n=No]?");
                key = Console.ReadKey().KeyChar;
                var subscriptionEnableDeadLetteringOnMessageExpiration = key == 'y' || key == 'Y';
                Console.WriteLine();

                // Read Subscription RequiresSession
                Console.ForegroundColor = ConsoleColor.Green;
                Console.Write("Subscriptions: ");
                Console.ForegroundColor = defaultColor;
                Console.Write("Set the ");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write("RequiresSession ");
                Console.ForegroundColor = defaultColor;
                Console.Write("to true [y=Yes, n=No]?");
                key = Console.ReadKey().KeyChar;
                var subscriptionRequiresSession = key == 'y' || key == 'Y';
                Console.WriteLine();

                // Get ServiceBusNamespaceClient for management operations
                var managementUri = 
                    ServiceBusEnvironment.CreateServiceUri("https", serviceNamespace, string.Empty);
                var tokenProvider = 
                    TokenProvider.CreateSharedAccessSignatureTokenProvider(issuerName, SASKey);
                var namespaceManager = new NamespaceManager(managementUri, tokenProvider);

                // Print Header
                Console.WriteLine();
                Console.WriteLine("Create Queues:");
                Console.WriteLine("--------------");

                // Create RequestQueue
                Console.Write("Creating ");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write(RequestQueue);
                Console.ForegroundColor = defaultColor;
                Console.WriteLine(" queue...");
                if (namespaceManager.QueueExists(RequestQueue))
                {
                    namespaceManager.DeleteQueue(RequestQueue);
                }
                namespaceManager.CreateQueue(new QueueDescription(RequestQueue)
                                {
                                    EnableBatchedOperations = queueEnabledBatchedOperations,
                                    EnableDeadLetteringOnMessageExpiration =
                                        queueEnableDeadLetteringOnMessageExpiration,
                                    RequiresDuplicateDetection = queueRequiresDuplicateDetection,
                                    RequiresSession = queueRequiresSession
                                });
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write(RequestQueue);
                Console.ForegroundColor = defaultColor;
                Console.WriteLine(" queue successfully created.");

                // Create ResponseQueue
                Console.Write("Creating ");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write(ResponseQueue);
                Console.ForegroundColor = defaultColor;
                Console.WriteLine(" queue...");
                if (namespaceManager.QueueExists(ResponseQueue))
                {
                    namespaceManager.DeleteQueue(ResponseQueue);
                }
                namespaceManager.CreateQueue(new QueueDescription(ResponseQueue)
                            {
                                EnableBatchedOperations = queueEnabledBatchedOperations,
                                EnableDeadLetteringOnMessageExpiration = 
                                    queueEnableDeadLetteringOnMessageExpiration,
                                RequiresDuplicateDetection = queueRequiresDuplicateDetection,
                                RequiresSession = queueRequiresSession
                            });
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write(ResponseQueue);
                Console.ForegroundColor = defaultColor;
                Console.WriteLine(" queue successfully created.");

                // Print Header
                Console.WriteLine();
                Console.WriteLine("Create Topics:");
                Console.WriteLine("--------------");

                // Create RequestTopic
                Console.Write("Creating ");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write(RequestTopic);
                Console.ForegroundColor = defaultColor;
                Console.WriteLine(" topic...");
                if (namespaceManager.TopicExists(RequestTopic))
                {
                    namespaceManager.DeleteTopic(RequestTopic);
                }
                namespaceManager.CreateTopic(new TopicDescription(RequestTopic)
                                        {
                                            EnableBatchedOperations = topicEnabledBatchedOperations,
                                            RequiresDuplicateDetection = topicRequiresDuplicateDetection
                                        });
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write(RequestTopic);
                Console.ForegroundColor = defaultColor;
                Console.WriteLine(" topic successfully created.");

                // Create ResponseTopic
                Console.Write("Creating ");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write(ResponseTopic);
                Console.ForegroundColor = defaultColor;
                Console.WriteLine(" topic...");
                if (namespaceManager.TopicExists(ResponseTopic))
                {
                    namespaceManager.DeleteTopic(ResponseTopic);
                }
                namespaceManager.CreateTopic(new TopicDescription(ResponseTopic)
                                    {
                                        EnableBatchedOperations = topicEnabledBatchedOperations,
                                        RequiresDuplicateDetection = topicRequiresDuplicateDetection
                                    });
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write(ResponseTopic);
                Console.ForegroundColor = defaultColor;
                Console.WriteLine(" topic successfully created.");

                // Print Header
                Console.WriteLine();
                Console.WriteLine("Create Subscriptions:");
                Console.WriteLine("--------------");

                // Create Request Subscription
                Console.Write("Creating ");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write(RequestSubscription);
                Console.ForegroundColor = defaultColor;
                Console.Write(" subscription for the ");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write(RequestTopic);
                Console.ForegroundColor = defaultColor;
                Console.WriteLine(" topic...");
                var ruleDescription = new RuleDescription(new SqlFilter("Country='Italy' and City='Milan'"))
                                          {
                                              Name = "$Default",
                                              Action = new SqlRuleAction("Set Area='Western Europe'")
                                          };
                var subscriptionDescription = new SubscriptionDescription(RequestTopic, RequestSubscription)
                {
                    EnableBatchedOperations = subscriptionnabledBatchedOperations,
                    EnableDeadLetteringOnFilterEvaluationExceptions = 
                            subscriptionEnableDeadLetteringOnFilterEvaluationExceptions,
                    EnableDeadLetteringOnMessageExpiration = 
                            subscriptionEnableDeadLetteringOnMessageExpiration,
                    RequiresSession = subscriptionRequiresSession
                };
                namespaceManager.CreateSubscription(subscriptionDescription, ruleDescription);
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write(RequestSubscription);
                Console.ForegroundColor = defaultColor;
                Console.Write(" subscription for the ");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write(RequestTopic);
                Console.ForegroundColor = defaultColor;
                Console.WriteLine(" topic successfully created.");

                // Create Response Subscription
                Console.Write("Creating ");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write(ResponseSubscription);
                Console.ForegroundColor = defaultColor;
                Console.Write(" subscription for the ");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write(ResponseTopic);
                Console.ForegroundColor = defaultColor;
                Console.WriteLine(" topic...");
                ruleDescription = new RuleDescription(new SqlFilter("Country='Italy' and City='Milan'"))
                                      {
                                          Action = new SqlRuleAction("Set Area='Western Europe'")
                                      };
                subscriptionDescription = new SubscriptionDescription(ResponseTopic, ResponseSubscription)
                {
                    EnableBatchedOperations = subscriptionnabledBatchedOperations,
                    EnableDeadLetteringOnFilterEvaluationExceptions = 
                            subscriptionEnableDeadLetteringOnFilterEvaluationExceptions,
                    EnableDeadLetteringOnMessageExpiration = 
                            subscriptionEnableDeadLetteringOnMessageExpiration,
                    RequiresSession = subscriptionRequiresSession
                };
                namespaceManager.CreateSubscription(subscriptionDescription, ruleDescription);
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write(ResponseSubscription);
                Console.ForegroundColor = defaultColor;
                Console.Write(" subscription for the ");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write(ResponseTopic);
                Console.ForegroundColor = defaultColor;
                Console.WriteLine(" topic successfully created.");
                Console.WriteLine();

                // Close the application
                Console.WriteLine("Press any key to continue ...");
                Console.ReadLine();
            }
            catch (Exception ex)
            {
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write("Exception: ");
                Console.ForegroundColor = defaultColor;
                Console.Write(ex.Message);
            }
        }
    }
}

noteNota
No probé todas las combinaciones posibles de propiedades para las colas y los temas de modo que demostración puede no funcionar según lo esperado con todas las configuraciones.

Comencé definiendo los contratos de datos para la solicitud y los mensajes de respuesta. Los contratos de datos proporcionan un mecanismo para asignar los tipos CLR .NET definidos en el código y los esquemas XML (XSD) definidos por la organización W3C (www.w3c.org). Los contratos de datos se publican en los metadatos del servicio, permitiendo a los clientes convertir la representación neutra, ajena a la tecnología, de los tipos de datos en sus representaciones nativas. Para obtener más información sobre los datos y los contratos, puede leer los artículos siguientes:

En mi solución, creé un proyecto denominado DataContracts para definir las clases que representan, respectivamente, los mensajes de solicitud y respuesta. Para mayor comodidad, incluí debajo su código.

CalculatorRequest Class

[Serializable]
[XmlType(TypeName = "CalculatorRequest", 
         Namespace = "http://windowsazure.cat.microsoft.com/samples/servicebus")]
[XmlRoot(ElementName = "CalculatorRequest", 
         Namespace = http://windowsazure.cat.microsoft.com/samples/servicebus, 
         IsNullable = false)]
[DataContract(Name = "CalculatorRequest", 
              Namespace = "http://windowsazure.cat.microsoft.com/samples/servicebus")]
public class CalculatorRequest
{
    #region Private Fields
    private OperationList operationList;
    #endregion

    #region Public Constructors
    public CalculatorRequest()
    {
        operationList = new OperationList();
    }

    public CalculatorRequest(OperationList operationList)
    {
        this.operationList = operationList;
    }
    #endregion

    #region Public Properties
    [XmlArrayItem("Operation", Type=typeof(Operation), IsNullable = false)]
    [DataMember(Order = 1)]
    public OperationList Operations
    {
        get
        {
            return operationList;
        }
        set
        {
            operationList = value;
        }
    } 
    #endregion
}

[CollectionDataContract(Name = "OperationList", 
                        Namespace = http://windowsazure.cat.microsoft.com/samples/servicebus, 
                        ItemName = "Operation")]
public class OperationList : List<Operation>
{
}

[Serializable]
[XmlType(TypeName = "Operation", 
         AnonymousType = true, 
         Namespace = "http://windowsazure.cat.microsoft.com/samples/servicebus")]
[DataContract(Name = "Operation", 
              Namespace = "http://windowsazure.cat.microsoft.com/samples/servicebus")]
public class Operation
{
    #region Private Fields
    private string op;
    private double operand1;
    private double operand2;
    #endregion

    #region Public Constructors
    public Operation()
    {
    }

    public Operation(string op,
                        double operand1,
                        double operand2)
    {
        this.op = op;
        this.operand1 = operand1;
        this.operand2 = operand2;
    }
    #endregion

    #region Public Properties
    [XmlElement]
    [DataMember(Order = 1)]
    public string Operator
    {
        get
        {
            return op;
        }
        set
        {
            op = value;
        }
    }

    [XmlElement]
    [DataMember(Order = 2)]
    public double Operand1
    {
        get
        {
            return operand1;
        }
        set
        {
            operand1 = value;
        }
    }

    [XmlElement]
    [DataMember(Order = 3)]
    public double Operand2
    {
        get
        {
            return operand2;
        }
        set
        {
            operand2 = value;
        }
    } 
    #endregion
}

CalculatorResponse Class

[Serializable]
[XmlType(TypeName = "CalculatorResponse", 
         Namespace = "http://windowsazure.cat.microsoft.com/samples/servicebus")]
[XmlRoot(ElementName = "CalculatorResponse", 
         Namespace = http://windowsazure.cat.microsoft.com/samples/servicebus, 
         IsNullable = false)]
[DataContract(Name = "CalculatorResponse", 
         Namespace = "http://windowsazure.cat.microsoft.com/samples/servicebus")]
public class CalculatorResponse
{
    #region Private Fields
    private string status;
    private ResultList resultList;
    #endregion

    #region Public Constructors
    public CalculatorResponse()
    {
        status = default(string);
        resultList = new ResultList();
    }

    public CalculatorResponse(string status)
    {
        this.status = status;
        resultList = new ResultList();
    }

    public CalculatorResponse(string status, ResultList resultList)
    {
        this.status = status;
        this.resultList = resultList;
    }
    #endregion

    #region Public Properties
    [XmlElement]
    [DataMember(Order = 1)]
    public string Status 
    {
        get 
        {
            return status;
        }
        set 
        {
            status = value;
        }
    }

    [XmlArrayItem("Result", Type=typeof(Result), IsNullable=false)]
    [DataMember(Order = 2)]
    public ResultList Results 
    {
        get 
        {
            return resultList;
        }
        set 
        {
            resultList = value;
        }
    }
    #endregion
}

[CollectionDataContract(Name = "ResultList", 
                        Namespace = http://windowsazure.cat.microsoft.com/samples/servicebus, 
                        ItemName = "Result")]
public class ResultList : List<Result>
{
}

[Serializable]
[XmlType(TypeName = "Result", 
         AnonymousType = true, 
         Namespace = "http://windowsazure.cat.microsoft.com/samples/servicebus")]
[DataContract(Name = "Result", 
              Namespace = "http://windowsazure.cat.microsoft.com/samples/servicebus")]
public class Result
{
    #region Private Fields
    private double value;
    private string error; 
    #endregion

    #region Public Constructors
    public Result()
    {
        value = default(double);
        error = default(string);
    }

    public Result(double value, string error)
    {
        this.value = value;
        this.error = error;
    }
    #endregion

    #region Public Properties
    [XmlElement]
    [DataMember(Order = 1)]
    public double Value
    {
        get
        {
            return value;
        }
        set
        {
            this.value = value;
        }
    }

    [XmlElement]
    [DataMember(Order = 2)]
    public string Error
    {
        get
        {
            return error;
        }
        set
        {
            error = value;
        }
    } 
    #endregion
}

El paso siguiente era definir los contratos de servicio que usa la aplicación cliente para intercambiar mensajes con CmdLets. Con este propósito, creé un nuevo proyecto en mi solución denominado ServiceContracts y definí dos interfaces de contrato de servicio que utiliza la aplicación cliente para enviar y recibir mensajes, respectivamente, de las entidades de mensajería de CmdLets. De hecho, creé dos versiones diferentes del contrato de servicio usado para recibir los mensajes de respuesta:

  • La interfaz ICalculatorResponse está diseñada para recibir los mensajes de respuesta de una cola o una suscripción sin sesión.

  • La interfaz ICalculatorResponseSessionful hereda del contrato de servicio ICalculatorResponse y se marca con el atributo (ServiceContract(SessionMode = SessionMode.Required)]. Este contrato de servicio está diseñado para recibir los mensajes de respuesta de una cola o una suscripción sin sesión.

Observe que los métodos definidos por todos los contratos son unidireccionales. Este es un requisito obligatorio.

ICalculatorRequest, ICalculatorResponse, ICalculatorResponseSessionful Interfaces

#region Using Directives
using System.ServiceModel;
using Microsoft.WindowsAzure.CAT.Samples.ServiceBus.MessageContracts;
#endregion

namespace Microsoft.WindowsAzure.CAT.Samples.ServiceBus.ServiceContracts
{
    [ServiceContract(Namespace = "http://windowsazure.cat.microsoft.com/samples/servicebus",
                     ConfigurationName = "ICalculatorRequest", 
                     SessionMode = SessionMode.Allowed)]
    public interface ICalculatorRequest
    {
        [OperationContract(Action = "SendRequest", IsOneWay = true)]
        [ReceiveContextEnabled(ManualControl = true)]
        void SendRequest(CalculatorRequestMessage calculatorRequestMessage);
    }

    [ServiceContract(Namespace = "http://windowsazure.cat.microsoft.com/samples/servicebus", 
                     ConfigurationName = "ICalculatorResponse",
                     SessionMode = SessionMode.Allowed)]
    public interface ICalculatorResponse
    {
        [OperationContract(Action = "ReceiveResponse", IsOneWay = true)]
        [ReceiveContextEnabled(ManualControl = true)]
        void ReceiveResponse(CalculatorResponseMessage calculatorResponseMessage);
    }

    [ServiceContract(Namespace = "http://windowsazure.cat.microsoft.com/samples/servicebus", 
                     ConfigurationName = "ICalculatorResponse"
                     SessionMode = SessionMode.Required)]
    public interface ICalculatorResponseSessionful : ICalculatorResponse
    {
    }
}

Ahora estamos listos para examinar el código de la aplicación cliente.

Puesto que la aplicación de Windows Forms intercambia mensajes con el servicio subyacente del flujo de trabajo WCF de forma asincrónica a través de las entidades de mensajería de CmdLets, actúa como cliente y como aplicación de servicio al mismo tiempo. Windows Forms utiliza WCF y NetMessagingBinding para realizar las siguientes acciones:

  1. Enviar mensajes de solicitud a requestqueue.

  2. Enviar mensajes de solicitud a requesttopic.

  3. Recibir mensajes de respuesta de responsequeue.

  4. Recibe los mensajes de respuesta de la suscripción ItalyMilan de responsetopic.

Comencemos por revisar el archivo de configuración de la aplicación cliente que desempeña un papel fundamental en la definición de los extremos del servicio y del cliente WCF que se usan para comunicarse con CmdLets.

Aplicación cliente App.Config


   1:  <?xml version="1.0"?>
   2:  <configuration>
   3:    <system.diagnostics>
   4:      <sources>
   5:        <source name="System.ServiceModel.MessageLogging"  
                     switchValue="Warning, ActivityTracing">
   6:          <listeners>
   7:            <add type="System.Diagnostics.DefaultTraceListener" 
                      name="Default">
   8:              <filter type="" />
   9:            </add>
  10:            <add name="ServiceModelMessageLoggingListener">
  11:              <filter type="" />
  12:            </add>
  13:          </listeners>
  14:        </source>
  15:      </sources>
  16:      <sharedListeners>
  17:        <add initializeData="C:\ServiceBusWFClient.svclog"
  18:             type="System.Diagnostics.XmlWriterTraceListener, System, 
  19:                   Version=2.0.0.0, Culture=neutral,
                        PublicKeyToken=b77a5c561934e089"
  20:             name="ServiceModelMessageLoggingListener"
  21:             traceOutputOptions="Timestamp">
  22:          <filter type="" />
  23:        </add>
  24:      </sharedListeners>
  25:      <trace autoflush="true" indentsize="4">
  26:        <listeners>
  27:          <clear/>
  28:          <add name="LogTraceListener"
  29:               type="Microsoft.WindowsAzure.CAT.Samples.ServiceBusAndWF.Client.LogTraceListener, Client"
  30:               initializeData="" />
  31:        </listeners>
  32:      </trace>
  33:    </system.diagnostics>
  34:    <startup>
  35:     <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
  36:    </startup>
  37:    <system.serviceModel>
  38:      <diagnostics>
  39:        <messageLogging logEntireMessage="true"
  40:                        logMalformedMessages="false"
  41:                        logMessagesAtServiceLevel="true"
  42:                        logMessagesAtTransportLevel="false" />
  43:      </diagnostics>
  44:      <behaviors>
  45:        <endpointBehaviors>
  46:          <behavior name="securityBehavior">
  47:            <transportClientEndpointBehavior>
  48:              <tokenProvider>
  49:                <sharedAccessSignature keyName="RootManageSharedAccessKey" key="SASKey" />
  51:              </tokenProvider>
  52:            </transportClientEndpointBehavior>
  53:          </behavior>
  54:        </endpointBehaviors>
  55:      </behaviors>
  56:      <bindings>
  57:        <netMessagingBinding>
  58:          <binding name="netMessagingBinding"
  59:                   sendTimeout="00:03:00"
  60:                   receiveTimeout="00:03:00"
  61:                   openTimeout="00:03:00"
  62:                   closeTimeout="00:03:00"
  63:                   sessionIdleTimeout="00:01:00"
  64:                   prefetchCount="-1">
  65:            <transportSettings batchFlushInterval="00:00:01" />
  66:          </binding>
  67:        </netMessagingBinding>
  68:      </bindings>
  69:      <client>
  70:        <!-- Invoke WF Service via Service Bus Queue -->
  71: <endpoint address="sb://NAMESPACE.servicebus.windows.net/requestqueue"
  72:                  behaviorConfiguration="securityBehavior" 
  73:                  binding="netMessagingBinding"
  74:                  bindingConfiguration="netMessagingBinding" 
  75:                  contract="ICalculatorRequest"
  76:                  name="requestQueueClientEndpoint" />
  77:        <!-- Invoke WF Service via Service Bus Topic -->
  78: <endpoint address="sb://NAMESPACE.servicebus.windows.net/requesttopic"
  79:                  behaviorConfiguration="securityBehavior"
  80:                  binding="netMessagingBinding"
  81:                  bindingConfiguration="netMessagingBinding"
  82:                  contract="ICalculatorRequest"
  83:                  name="requestTopicClientEndpoint" />
  84:      </client>
  85:      <services>
  86:        <service name="ResponseHandlerService">
  87: <endpoint address="sb://NAMESPACE.servicebus.windows.net/responsequeue"
  88:                    behaviorConfiguration="securityBehavior"
  89:                    binding="netMessagingBinding"
  90:                    bindingConfiguration="netMessagingBinding"
  91:                    name="responseQueueServiceEndpoint"
  92:                    contract="ICalculatorResponse" />
  93: <endpoint address="sb://NAMESPACE.servicebus.windows.net/responsetopic"
  94:                    listenUri="sb://NAMESPACE.servicebus.windows.net/responsetopic/Subscriptions/ItalyMilan"
  95:                    behaviorConfiguration="securityBehavior"
  96:                    binding="netMessagingBinding"
  97:                    bindingConfiguration="netMessagingBinding"
  98:                    name="responseSubscriptionServiceEndpoint"
  99:                    contract="ICalculatorResponse" />
 100:        </service>
 101:      </services>
 102:    </system.serviceModel>
 103:  </configuration>

A continuación puede encontrar una breve descripción de los elementos y las secciones principales del archivo de configuración:

  • Las líneas [3-32] definen un agente de escucha de seguimiento personalizado denominado LogTraceListener que ResponseHandlerService usa para escribir el mensaje de respuesta al control del registro de la aplicación de Windows Forms.

  • Las líneas [34-36] de la sección de inicio especifican qué versiones de Common Language Runtime admite la aplicación.

  • Las líneas [46-53] contienen la definición del securityBehavior utilizado por los extremos de cliente y de servicio para autenticarse con el servicio Access Control. Concretamente, el TransportClientEndpointBehavior se utiliza para definir las credenciales del secreto compartido. Para obtener más información sobre cómo recuperar las credenciales del Portal de administración de Azure, vea el cuadro a continuación.

  • Las líneas [57-67] contienen la configuración del NetMessagingBinding utilizado por los extremos de cliente y de servicio para intercambiar mensajes con CmdLets.

  • Las líneas [71-76] contienen la definición del requestQueueClientEndpoint utilizado por la aplicación para enviar mensajes de solicitud a requestqueue. La concatenación de la dirección URL del espacio de nombres de servicio y el nombre de la cola proporcionan la dirección del extremo cliente.

  • Las líneas [78-83] contienen la definición del requestTopicClientEndpoint utilizado por la aplicación para enviar mensajes de solicitud a requesttopic. La concatenación de la dirección URL del espacio de nombres de servicio y el nombre del tema proporcionan la dirección del extremo cliente.

  • Las líneas [87-92] contienen la definición del responseQueueServiceEndpoint utilizado por la aplicación para recibir mensajes de respuesta de requestqueue. La concatenación de la dirección URL del espacio de nombres de servicio y el nombre de la cola proporcionan la dirección del extremo de servicio.

  • Las líneas [93-99] contienen la definición del responseSubscriptionServiceEndpoint utilizado por la aplicación para recibir mensajes de respuesta de la suscripción ItalyMilan para responsetopic. Cuando se define un extremo de servicio WCF que usa NetMessagingBinding para recibir los mensajes de una suscripción, tiene que seguir de la forma siguiente (para obtener más información sobre esto, vea el cuadro a continuación):

    • Como valor del atributo address, debe especificar la dirección URL del tema al que la suscripción pertenece. La concatenación de la dirección URL del espacio de nombres de servicio y el nombre del tema proporcionan la dirección del tema.

    • Como valor del atributo listenUri, debe especificar la dirección URL de la suscripción. La concatenación de la dirección URL del tema, la cadena Suscripciones y el nombre de la suscripción definen la dirección URL de la suscripción.

    • Asigne el valor Explicit al atributo listenUriMode. El valor predeterminado de listenUriMode es Explicit, por lo que este valor es opcional.

noteNota
Al configurar un extremo de servicio WCF para utilizar los mensajes de una cola o una suscripción con sesión, el contrato de servicio debe admitir sesiones. Por tanto, en nuestro ejemplo, al configurar los extremos responseQueueServiceEndpoint o responseSubscriptionServiceEndpoint para recibir, respectivamente, una cola y una suscripción con sesión, tiene que reemplazar el contrato de servicio ICalculatorResponse con la interfaz de contrato con sesión ICalculatorResponseSessionful. Para obtener más información, vea la sección sobre Contratos de servicio posteriormente en el artículo.

noteNota
CmdLets admite tres tipos distintos de esquemas de credenciales: SAML, Secreto compartido y Token de web simple, pero esta versión del Explorador de Service Bus solo admite credenciales Secreto compartido. Sin embargo, puede ampliar fácilmente mi código para admitir otros esquemas de credenciales. Puede recuperar la clave secreto-emisor del Portal de administración de Azure haciendo clic en Ver después de seleccionar el espacio de nombres en la sección Service Bus.

De este modo, se abre el cuadro de diálogo modal en el que puede recuperar la clave haciendo clic en Copiar al Portapapeles resaltado en rojo.

noteNota
Por convención, el nombre del Emisor predeterminado siempre es propietario.

noteNota
Cuando se define un extremo de servicio WCF que utilice el NetMessagingBinding para recibir los mensajes de una suscripción, si comete la equivocación de asignar la dirección URL de la suscripción al atributo de dirección del extremo de servicio (como se explica en la configuración a continuación), en tiempo de ejecución se producirá una FaultException como la siguiente:

The message with To 'sb://NAMESPACE.servicebus.windows.net/responsetopic' cannot be processed at 
the receiver, due to an AddressFilter mismatch at the EndpointDispatcher. 
Check that the sender and receiver's EndpointAddresses agree."}

Configuración incorrecta


<?xml version="1.0"?>
<configuration>
  ...
  <system.serviceModel>
    ...
    <services>
      <service name="Microsoft.WindowsAzure.CAT.Samples.ServiceBus.Service.ResponseHandlerService">
        <endpoint address="sb://NAMESPACE.servicebus.windows.net/responsetopic/Subscriptions/ItalyMilan"
                  behaviorConfiguration="securityBehavior"
                  binding="netMessagingBinding"
                  bindingConfiguration="netMessagingBinding"
                  name="responseSubscriptionServiceEndpoint"
                  contract="ICalculatorResponse" />
      </service>
    </services>
  </system.serviceModel>
</configuration>

El error se debe al hecho de que el encabezado A WS-Addressing del mensaje contiene la dirección del tema y no la dirección de la suscripción:


<:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
  <s:Header>
    <a:Action s:mustUnderstand="1">ReceiveResponse</a:Action>
    <a:MessageID>urn:uuid:64cb0b06-1622-4920-a035-c27b610cfcaf</a:MessageID>
    <a:To s:mustUnderstand="1">sb://NAMESPACE.servicebus.windows.net/responsetopic</a:To>
  </s:Header>
  <s:Body>... stream ...</s:Body>
</s:Envelope>

Para configurar correctamente el extremo del servicio para recibir los mensajes de una suscripción, tiene que continuar de la forma siguiente:

  • Como valor del atributo address, debe especificar la dirección URL del tema al que la suscripción pertenece. La concatenación de la dirección URL del espacio de nombres de servicio y el nombre del tema proporcionan la dirección del tema.

  • Como valor del atributo listenUri, debe especificar la dirección URL de la suscripción. La concatenación de la dirección URL del tema, la cadena Suscripciones y el nombre de la suscripción definen la dirección URL de la suscripción.

  • Asigne el valor Explicit al atributo listenUriMode. El valor predeterminado de listenUriMode es Explicit, por lo que este valor es opcional.

Vea la página siguiente en MSDN para obtener una descripción de los atributos address, listenUri y listenUriMode.

Configuración correcta


<?xml version="1.0"?>
<configuration>
  ...
  <system.serviceModel>
    ...
    <services>
      <service name="Microsoft.WindowsAzure.CAT.Samples.ServiceBus.Service.ResponseHandlerService">
        <endpoint address="sb://NAMESPACE.servicebus.windows.net/responsetopic"
                  listenUri="sb://NAMESPACE.servicebus.windows.net/responsetopic/Subscriptions/ItalyMilan"
                  behaviorConfiguration="securityBehavior"
                  binding="netMessagingBinding"
                  bindingConfiguration="netMessagingBinding"
                  name="subscriptionEndpoint"
                  contract="ICalculatorResponse" />
      </service>
    </services>
  </system.serviceModel>
</configuration>

Para realizar la misma tarea a través de la API, tiene que establecer correctamente el valor de las direcciones Address, ListenUri y ListenUriMode de la instancia de ServiceEndpoint, como se indica en esta nota.

La siguiente tabla muestra el código que la aplicación cliente usa para iniciar el ResponseHandlerService usado para leer los mensajes de respuesta de responsequeue y la suscripción ItalyMilan de responsetopic. Examinaremos el código del servicio en la sección siguiente.


private void StartServiceHost()
{
    try
    {
        // Creating the service host object as defined in config
        var serviceHost = new ServiceHost(typeof(ResponseHandlerService));
                
        // Add ErrorServiceBehavior for handling errors encounter by servicehost during execution.
        serviceHost.Description.Behaviors.Add(new ErrorServiceBehavior());


        foreach (var serviceEndpoint in serviceHost.Description.Endpoints)
        {
            if (serviceEndpoint.Name == "responseQueueServiceEndpoint")
            {
                responseQueueUri = serviceEndpoint.Address.Uri.AbsoluteUri;
                WriteToLog(string.Format(ServiceHostListeningOnQueue,
                                        serviceEndpoint.Address.Uri.AbsoluteUri));
            }
            if (serviceEndpoint.Name == "responseSubscriptionServiceEndpoint")
            {
                responseTopicUri = serviceEndpoint.Address.Uri.AbsoluteUri;
                WriteToLog(string.Format(ServiceHostListeningOnSubscription,
                                            serviceEndpoint.ListenUri.AbsoluteUri));
            }
        }

        // Start the service host
        serviceHost.Open();
        WriteToLog(ServiceHostSuccessfullyOpened);
    }
    catch (Exception ex)
    {
        mainForm.HandleException(ex);
    }
}

La siguiente imagen muestra la interfaz de usuario de la aplicación cliente:

Temas-de-colas-del-bus-de-servicio-Servicio-de-flujo-de-trabajo-de-WCF11

Los botones de opción incluidos en el grupo Método de solicitud permiten elegir si se envía el mensaje de solicitud a requestqueue o a requesttopic, mientras que los botones de opción incluidos en el grupo Método de respuesta permiten seleccionar si recibir la respuesta de responsequeue o la suscripción ItalyMilan de responsetopic. Para comunicar la selección a la aplicación de BizTalk subyacente, la aplicación utiliza un objeto BrokeredMessageProperty para asignar el valor de los campos privados responseQueueUri o responseTopicUri a la propiedad ReplyTo. La tabla siguiente contiene el código del método utilizado por la aplicación cliente para enviar el mensaje a CmdLets. Para mayor comodidad, los comentarios se han agregado al código para facilitar su descripción.


private void SendRequestMessageUsingWCF(string endpointConfigurationName)
{
    try
    {
        if (string.IsNullOrEmpty(endpointConfigurationName))
        {
            WriteToLog(EndpointConfigurationNameCannotBeNull);
            return;
        }

        // Set the wait cursor
        Cursor = Cursors.WaitCursor;

        // Make sure that the request message contains at least an operation
        if (operationList == null ||
            operationList.Count == 0)
        {
            WriteToLog(OperationListCannotBeNull);
            return;
        }

        // Create warning collection
        var warningCollection = new List<string>();

        // Create request message
        var calculatorRequest = new CalculatorRequest(operationList);
        var calculatorRequestMessage = new CalculatorRequestMessage(calculatorRequest);

        // Create the channel factory for the currennt client endpoint
        // and cache it in the channelFactoryDictionary
        if (!channelFactoryDictionary.ContainsKey(endpointConfigurationName))
        {
            channelFactoryDictionary[endpointConfigurationName] = 
                new ChannelFactory<ICalculatorRequest>(endpointConfigurationName);
        }

        // Create the channel for the currennt client endpoint
        // and cache it in the channelDictionary
        if (!channelDictionary.ContainsKey(endpointConfigurationName))
        {
            channelDictionary[endpointConfigurationName] = 
                channelFactoryDictionary[endpointConfigurationName].CreateChannel();
        }

        // Use the OperationContextScope to create a block within which to access the current OperationScope
        using (new OperationContextScope((IContextChannel)channelDictionary[endpointConfigurationName]))
        {
            // Create a new BrokeredMessageProperty object
            var brokeredMessageProperty = new BrokeredMessageProperty();

            // Read the user defined properties and add them to the  
            // Properties collection of the BrokeredMessageProperty object
            foreach (var e in propertiesBindingSource.Cast<PropertyInfo>())
            {
                try
                {
                    e.Key = e.Key.Trim();
                    if (e.Type != StringType && e.Value == null)
                    {
                        warningCollection.Add(string.Format(CultureInfo.CurrentUICulture, 
                                                            PropertyValueCannotBeNull, e.Key));
                    }
                    else
                    {
                        if (brokeredMessageProperty.Properties.ContainsKey(e.Key))
                        {
                            brokeredMessageProperty.Properties[e.Key] = 
                                ConversionHelper.MapStringTypeToCLRType(e.Type, e.Value);
                        }
                        else
                        {
                            brokeredMessageProperty.Properties.Add(e.Key, 
                                ConversionHelper.MapStringTypeToCLRType(e.Type, e.Value));
                        }
                    }
                }
                catch (Exception ex)
                {
                    warningCollection.Add(string.Format(CultureInfo.CurrentUICulture, 
                        PropertyConversionError, e.Key, ex.Message));
                }
            }

            // if the warning collection contains at least one or more items,
            // write them to the log and return immediately
            StringBuilder builder;
            if (warningCollection.Count > 0)
            {
                builder = new StringBuilder(WarningHeader);
                var warnings = warningCollection.ToArray<string>();
                for (var i = 0; i < warningCollection.Count; i++)
                {
                    builder.AppendFormat(WarningFormat, warnings[i]);
                }
                mainForm.WriteToLog(builder.ToString());
                return;
            }

            // Set the BrokeredMessageProperty properties
            brokeredMessageProperty.Label = txtLabel.Text;
            brokeredMessageProperty.MessageId = Guid.NewGuid().ToString();
            brokeredMessageProperty.SessionId = sessionId;
            brokeredMessageProperty.ReplyToSessionId = sessionId;
            brokeredMessageProperty.ReplyTo = responseQueueRadioButton.Checked
                                                ? responseQueueUri
                                                : responseTopicUri;
            OperationContext.Current.OutgoingMessageProperties.Add(BrokeredMessageProperty.Name, 
                                                                    brokeredMessageProperty);
                    
            // Send the request message to the requestqueue or requesttopic
            var stopwatch = new Stopwatch();
            try
            {
                stopwatch.Start();
                channelDictionary[endpointConfigurationName].SendRequest(calculatorRequestMessage);
            }
            catch (CommunicationException ex)
            {
                if (channelFactoryDictionary[endpointConfigurationName] != null)
                {
                    channelFactoryDictionary[endpointConfigurationName].Abort();
                    channelFactoryDictionary.Remove(endpointConfigurationName);
                    channelDictionary.Remove(endpointConfigurationName);
                }
                HandleException(ex);
            }
            catch (Exception ex)
            {
                if (channelFactoryDictionary[endpointConfigurationName] != null)
                {
                    channelFactoryDictionary[endpointConfigurationName].Abort();
                    channelFactoryDictionary.Remove(endpointConfigurationName);
                    channelDictionary.Remove(endpointConfigurationName);
                }
                HandleException(ex);
            }
            finally
            {
                stopwatch.Stop();
            }
            // Log the request message and its properties
            builder = new StringBuilder();
            builder.AppendLine(string.Format(CultureInfo.CurrentCulture,
                    MessageSuccessfullySent,
                    channelFactoryDictionary[endpointConfigurationName].Endpoint.Address.Uri.AbsoluteUri,
                    brokeredMessageProperty.MessageId,
                    brokeredMessageProperty.SessionId,
                    brokeredMessageProperty.Label,
                    stopwatch.ElapsedMilliseconds));
            builder.AppendLine(PayloadFormat);
            for (var i = 0; i < calculatorRequest.Operations.Count; i++)
            {
                builder.AppendLine(string.Format(RequestFormat,
                                                    i + 1,
                                                    calculatorRequest.Operations[i].Operand1,
                                                    calculatorRequest.Operations[i].Operator,
                                                    calculatorRequest.Operations[i].Operand2));
            }
            builder.AppendLine(SentMessagePropertiesHeader);
            foreach (var p in brokeredMessageProperty.Properties)
            {
                builder.AppendLine(string.Format(MessagePropertyFormat,
                                                    p.Key,
                                                    p.Value));
            }
            var traceMessage = builder.ToString();
            WriteToLog(traceMessage.Substring(0, traceMessage.Length - 1));
        }
    }
    catch (Exception ex)
    {
        // Handle the exception
        HandleException(ex);
    }
    finally
    {
        // Restoire the defaulf cursor
        Cursor = Cursors.Default;
    }
}

La siguiente tabla contiene el código del servicio WCF que la aplicación cliente usa para recuperar y registrar los mensajes de respuesta de responsequeue y de la suscripción ItalyMilan de responsetopic. Para obtener este resultado, el servicio expone dos extremos diferentes, cada uno de los cuales usa el NetMessagingBinding y recibe los mensajes de una de las dos colas. De hecho, cada suscripción se puede considerar como una cola virtual que obtiene copias de los mensajes publicados para el tema al que pertenecen. La tabla siguiente muestra el código de la clase ResponseHandlerService. Como puede observar, el servicio recupera la propiedad BrokeredMessageProperty de la colección Properties del mensaje WCF entrante y utiliza este objeto para tener acceso a las propiedades del mensaje de respuesta. Puesto que en el contrato de servicio ICalculatorResponse, el método ReceiveResponse se decora con [ReceiveContextEnabled (ManualControl = true)], el reconocimiento de la recepción debe señalarse explícitamente mediante el método del servicio. Esto requiere que el servicio invoque explícitamente al método ReceiveContext.Complete para confirmar la operación de recepción. De hecho, como se indicó al comienzo del artículo, cuando la propiedad ManualControl está establecida en true, el mensaje recibido del canal se entrega a la operación de servicio con un bloqueo del mensaje. Es responsabilidad de la implementación del servicio llamar a Complete(TimeSpan) o a Abandon(TimeSpan) para señalizar la finalización de la recepción del mensaje. Si no se puede llamar a alguno de estos métodos, el bloqueo se mantiene en el mensaje hasta que transcurre el intervalo de tiempo de espera del bloqueo. Una vez que el bloqueo se libera (ya sea llamando a Abandon(TimeSpan) o porque transcurra el tiempo de espera del bloqueo), el mensaje se envía de nuevo desde el canal al servicio. Al llamar a Complete(TimeSpan), se marca el mensaje como que se ha recibido correctamente.

Clase ResponseHandlerService


      [ServiceBehavir(Namespace = "http://windwsazure.cat.micrsft.cm/samples/servicebus", 
                 CnfiguratinName = "RespnseHandlerService")]
 public class RespnseHandlerService : ICalculatrRespnseSessinful
 {
     #regin Private Cnstants
     //***************************
     // Frmats
     //***************************
     private cnst string MessageSuccessfullyReceived = "Respnse Message Received:\n - EndpintUrl:[{0}]\n - CrrelatinId=[{1}]\n - SessinId=[{2}]\n - Label=[{3}]";
     private cnst string ReceivedMessagePrpertiesHeader = "Prperties:";
     private cnst string PayladFrmat = "Paylad:";
     private cnst string StatusFrmat = " - Status=[{0}]";
     private cnst string ResultFrmat = " - Result[{0}]: Value=[{1}] Errr=[{2}]";
     private cnst string MessagePrpertyFrmat = " - Key=[{0}] Value=[{1}]";
 
     //***************************
     // Cnstants
     //***************************
     private cnst string Empty = "EMPTY";
     #endregin
 
     #regin Public peratins
     [peratinBehavir]
     public vid ReceiveRespnse(CalculatrRespnse calculatrRespnse)
     {
         try
         {
             // Get the message prperties
             var incmingPrperties = peratinCntext.Current.IncmingMessagePrperties;
             if (calculatrRespnse != null)
             {
                 var brkeredMessagePrperty = incmingPrperties[BrkeredMessagePrperty.Name] as BrkeredMessagePrperty;
 
                 // Trace the respnse message
                 var builder = new StringBuilder();
                 if (brkeredMessagePrperty != null)
                     builder.AppendLine(string.Frmat(MessageSuccessfullyReceived, 
                                                         peratinCntext.Current.Channel.LcalAddress.Uri.AbsluteUri,
                                                         brkeredMessagePrperty.CrrelatinId ?? Empty,
                                                         brkeredMessagePrperty.SessinId ?? Empty,
                                                         brkeredMessagePrperty.Label ?? Empty));
                 builder.AppendLine(PayladFrmat);
                 builder.AppendLine(string.Frmat(StatusFrmat,
                                                     calculatrRespnse.Status));
                 if (calculatrRespnse.Results != null && 
                     calculatrRespnse.Results.Cunt > 0)
                 {
                     fr (int i = 0; i < calculatrRespnse.Results.Cunt; i++)
                     {
                         builder.AppendLine(string.Frmat(ResultFrmat, 
                                                             i + 1, 
                                                             calculatrRespnse.Results[i].Value,
                                                             calculatrRespnse.Results[i].Errr));
                     }
                 }
                 builder.AppendLine(ReceivedMessagePrpertiesHeader);
                 if (brkeredMessagePrperty != null)
                 {
                     freach (var prperty in brkeredMessagePrperty.Prperties)
                     {
                         builder.AppendLine(string.Frmat(MessagePrpertyFrmat,
                                                             prperty.Key,
                                                             prperty.Value));
                     }
                 }
                 var traceMessage = builder.TString();
                 Trace.WriteLine(traceMessage.Substring(0, traceMessage.Length - 1));
             }
             //Cmplete the Message
             ReceiveCntext receiveCntext;
             if (ReceiveCntext.TryGet(incmingPrperties, ut receiveCntext))
             {
                 receiveCntext.Cmplete(TimeSpan.FrmSecnds(10.0d));
             }
             else
             {
                 thrw new InvalidperatinExceptin("Receiver is in peek lck mde but receive cntext is nt available!");
             }
         }
         catch (Exceptin ex)
         {
             Trace.WriteLine(ex.Message);
         }
     } 
     #endregin
 }

Los servicios del flujo de trabajo WCF proporcionan un entorno productivo para crear operaciones o servicios de ejecución prolongada y perdurables. Los servicios de flujo de trabajo se implementan utilizando actividades WF que pueden utilizar WCF para enviar y recibir datos. Explicar detalladamente cómo crear un servicio de flujo de trabajo WCF no es un objetivo del presente artículo. Para obtener más información de los servicios de flujo de trabajo WCF, vea los siguientes artículos:

WF 4.0 incluyó por primera vez actividades de mensajería que permiten a los desarrolladores exponer o consumir servicios WCF de una forma sencilla y flexible. Concretamente, las actividades de mensajería permiten a los flujos de trabajo enviar datos a otros sistemas (Send, SendReply) y recibir datos de otros sistemas (Receive, ReceiveReply) mediante WCF. Sin embargo, estas actividades ocultan una gran parte de los entresijos de WCF. En concreto, las actividades de mensajería no proporcionan acceso al actual OperationContext que se puede utilizar para realizar la operación siguiente:

  • En el lado del envío, el OperationContext se puede utilizar para incluir encabezados de mensaje adicionales de sobre SOAP o para agregar propiedades de mensaje al mensaje saliente.

  • En el lado de la recepción, el OperationContext se puede utilizar para recuperar información de la seguridad y las propiedades del mensaje para el mensaje entrante.

Como vimos al principio del artículo, cuando una aplicación utiliza WCF y el NetMessagingBinding para enviar un mensaje a una cola o un tema, el mensaje se ajusta en un elemento de sobre y se codifica. Para establecer las propiedades específicas de BrokeredMessage, debe crear un objeto BrokeredMessageProperty, establecer las propiedades en él y agregarlo a la colección Properties del Message WCF. Por consiguiente, para recuperar la BrokeredMessageProperty de un mensaje entrante o agregar una BrokeredMessageProperty a la colección de propiedades de un mensaje WCF saliente, hay que extender la funcionalidad que las actividades de mensajería proporcionan directamente. Afortunadamente, WF 4.0 permite ampliar el comportamiento del tiempo de ejecución de las actividades de mensajería con ISendMessageCallback e IReceiveMessageCallback. En concreto:

  • La interfaz IReceiveMessageCallback implementa una devolución de llamada que se ejecutará cuando la actividad de recepción reciba un mensaje de servicio.

  • La interfaz ISendMessageCallback.interface implementa una devolución de llamada a se llama justo antes de que la actividad Send envíe un mensaje por el cable.

En mi demostración, usé estos puntos de extensibilidad para crear una NativeActivity personalizada denominada BrokeredMessagePropertyActivity que permite:

  • Obtener la BrokeredMessageProperty de las propiedades de un mensaje WCF entrante.

  • Establecer la BrokeredMessageProperty de un mensaje WCF saliente.

Roman Kiss, en su artículo, siguió el mismo enfoque y creó una actividad más sofisticada que expone una propiedad para cada propiedad expuesta por la clase BrokeredMessageProperty. Es recomendable leer su artículo; explica básicamente una manera alternativa de implementar la misma técnica que describo en el artículo actual.

Para mayor comodidad, incluí el código de BrokeredMessagePropertyActivity en la tabla siguiente:

Clase BrokeredMessagePropertyActivity


[Designer(typeof(BrokeredMessagePropertyActivityDesigner))]
public class BrokeredMessagePropertyActivity : NativeActivity
{
    #region Public Properties
    [Browsable(false)]
    public Activity Body { get; set; }
    public InOutArgument<BrokeredMessageProperty> BrokeredMessageProperty { get; set; }
    #endregion

    #region NativeActivity Overriden Methods
    protected override void CacheMetadata(NativeActivityMetadata metadata)
    {
        metadata.AddChild(Body);
        base.CacheMetadata(metadata);
    }

    protected override void Execute(NativeActivityContext context)
    {
        // Add the BrokeredMessagePropertyMessageCallback implementation as an Execution property 
        var value = context.GetValue(BrokeredMessageProperty);
        context.Properties.Add(typeof(BrokeredMessagePropertyCallback).Name,
                                new BrokeredMessagePropertyCallback(value));
        context.ScheduleActivity(Body, OnBodyCompleted);
    }

    private void OnBodyCompleted(NativeActivityContext context, ActivityInstance instance)
    {
        // Sets the value of the BrokeredMessageProperty argument
        var callback = context.Properties.Find(typeof(BrokeredMessagePropertyCallback).Name) as BrokeredMessagePropertyCallback;
        if (callback != null)
        {
            context.SetValue(BrokeredMessageProperty, callback.BrokeredMessageProperty);
        }
    } 
    #endregion  
}

Clase BrokeredMessagePropertyCallback


[DataContract]
public class BrokeredMessagePropertyCallback : IReceiveMessageCallback, ISendMessageCallback
{
    #region Public Properties
    [DataMember]
    public BrokeredMessageProperty BrokeredMessageProperty { get; set; } 
    #endregion

    #region Public Constructors
    public BrokeredMessagePropertyCallback(BrokeredMessageProperty property)
    {
        BrokeredMessageProperty = property;
    }
    #endregion

    #region IReceiveMessageCallback Methods
    public void OnReceiveMessage(OperationContext operationContext, ExecutionProperties activityExecutionProperties)
    {
        try
        {
            // Get the BrokeredMessageProperty from an inbound message
            var incomingMessageProperties = operationContext.IncomingMessageProperties;
            BrokeredMessageProperty = incomingMessageProperties[BrokeredMessageProperty.Name] as BrokeredMessageProperty;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    } 
    #endregion

    #region ISendMessageCallback Methods
    public void OnSendMessage(OperationContext operationContext)
    {
        // Set the BrokeredMessageProperty for an outbound message 
        if (BrokeredMessageProperty != null)
        {
            operationContext.OutgoingMessageProperties.Add(BrokeredMessageProperty.Name, BrokeredMessageProperty);
        }
    } 
    #endregion
}

Se puede usar BrokeredMessagePropertyActivity para ajustar las actividades Receive o Send. Así, se proporciona automáticamente acceso a la BrokeredMessageProperty que se pueden usar para leer y escribir las propiedades definidas por el usuario y explícitas de un BrokeredMessage.

Para preparar la solicitud entrante y generar una respuesta, creé la actividad de código personalizado denominada CalculatorActivity, cuyo código se muestra en la siguiente tabla.

Clase CalculatorActivity


#region Using Directives
using System;
using System.Activities;
using System.ComponentModel;
using Microsoft.ServiceBus.Messaging;
using Microsoft.WindowsAzure.CAT.Samples.ServiceBusAndWF.DataContracts;
#endregion

namespace Microsoft.WindowsAzure.CAT.Samples.ServiceBusAndWF.WorkflowActivities
{
    /// <summary>
    /// This class can be used to process a calculator request.
    /// </summary>
    [Designer(typeof(CalculatorActivityDesigner))]
    public sealed class CalculatorActivity : CodeActivity
    {
        #region Private Constants
        private const string Empty = "Empty";
        private const string MessageId = "MessageId";
        private const string SessionId = "SessionId";
        private const string CorrelationId = "CorrelationId";
        private const string Label = "Label";
        private const string ReplyTo = "ReplyTo";
        private const string Source = "Source";
        private const string CalculatoService = "CalculatoService";
        private const string Ok = "ok";
        private const string Failed = "Failed";
        private const string OperationsHeader = "Operations:";
        private const string OperationFormat = "{0} {1} {2} = {3}";
        private const string RequestMessagePropertiesHeader = "Request Message Properties:";
        private const string ResponseMessagePropertiesHeader = "Response Message Properties:";
        private const string RequestMessageUserDefinedPropertiesHeader = "Request Message User-Defined Properties:";
        private const string ResponseMessageUserDefinedPropertiesHeader = "Response Message User-Defined Properties:";
        private const string PropertyFormat = "Key=[{0}] Value=[{1}]";
        private const string OperationUnknownErrorMessageFormat = "The operation failed because the operator {0} is unknown.";

        #endregion

        #region Activity Arguments
        [DefaultValue(null)]
        public InArgument<CalculatorRequest> CalculatorRequest { get; set; }
        [DefaultValue(null)]
        public InArgument<BrokeredMessageProperty> InboundBrokeredMessageProperty 
                                                   { get; set; }
        public OutArgument<BrokeredMessageProperty> OutboundBrokeredMessageProperty 
                                                   { get; set; }
        public OutArgument<CalculatorResponse> CalculatorResponse { get; set; }
        #endregion

        #region Private Fields
        private readonly string line = new string('-', 79);
        #endregion

        #region Protected Methods
        /// <summary>
        /// Processes a calculator calculatorRequest.
        /// </summary>
        /// <param name="context">The execution context under which the activity executes.</param>
        protected override void Execute(CodeActivityContext context)
        {
            // Obtain the runtime value of the CalculatorRequest input arguments
            var calculatorRequest = context.GetValue(CalculatorRequest);
            var calculatorResponse = new CalculatorResponse();
            if (calculatorRequest == null)
            {
                context.SetValue(CalculatorResponse, calculatorResponse);
                return;
            }
            // Print the properties of the inbound BrokeredMessageProperty
            var brokeredMessageProperty = context.GetValue(InboundBrokeredMessageProperty);
            if (brokeredMessageProperty != null)
            {
                Console.WriteLine(RequestMessagePropertiesHeader);
                Console.WriteLine(line);
                Console.WriteLine(string.Format(PropertyFormat, 
                                                MessageId, 
                                      brokeredMessageProperty.MessageId ?? Empty));
                Console.WriteLine(string.Format(PropertyFormat, 
                                                SessionId, 
                                      brokeredMessageProperty.SessionId ?? Empty));
                Console.WriteLine(string.Format(PropertyFormat, 
                                                ReplyTo, 
                                      brokeredMessageProperty.ReplyTo ?? Empty));
                Console.WriteLine(string.Format(PropertyFormat, 
                                                Label, 
                                      brokeredMessageProperty.Label ?? Empty));
                Console.WriteLine(line);
                if (brokeredMessageProperty.Properties.Count > 0)
                {
                    Console.WriteLine(RequestMessageUserDefinedPropertiesHeader);
                    Console.WriteLine(line);
                    foreach (var property in brokeredMessageProperty.Properties)
                    {
                        Console.WriteLine(string.Format(PropertyFormat, 
                                                        property.Key, 
                                                        property.Value));
                    }
                    Console.WriteLine(line);
                }
            }

            // Process the request message and create a response message
            string error = null;
            calculatorResponse.Status = Ok;
            if (calculatorRequest.Operations.Count > 0)
            {
                Console.WriteLine(OperationsHeader);
                Console.WriteLine(line);
                foreach (var operation in calculatorRequest.Operations)
                {
                    double value = 0;
                    var succeeded = true;
                    switch (operation.Operator)
                    {
                        case "+":
                            value = operation.Operand1 + operation.Operand2;
                            break;
                        case "-":
                            value = operation.Operand1 - operation.Operand2;
                            break;
                        case "*":
                        case "x":
                            value = operation.Operand1 * operation.Operand2;
                            break;
                        case "/":
                        case "\\":
                        case ":":
                            value = operation.Operand1 / operation.Operand2;
                            break;
                        default:
                            error = string.Format(OperationUnknownErrorMessageFormat,
                                                  operation.Operator);
                            succeeded = false;
                            calculatorResponse.Status = Failed;
                            break;
                    }
                    Console.WriteLine(succeeded
                                          ? string.Format(OperationFormat, operation.Operand1, operation.Operator,
                                                          operation.Operand2, value)
                                          : error);
                    calculatorResponse.Results.Add(new Result(value, error));
                }
                Console.WriteLine(line);
            }
            context.SetValue(CalculatorResponse, calculatorResponse);

            // Create a new BrokeredMessageProperty for the reply message
            var replyBrokeredMessageProperty = new BrokeredMessageProperty
            {
               MessageId = Guid.NewGuid().ToString(),
               CorrelationId = brokeredMessageProperty != null ? 
                               brokeredMessageProperty.MessageId :
                               null,
               SessionId = brokeredMessageProperty != null ?
                           brokeredMessageProperty.ReplyToSessionId :
                           null,
               Label = brokeredMessageProperty != null ?
                       brokeredMessageProperty.Label :
                       null
            };
            if (brokeredMessageProperty != null)
            {
                foreach (var property in brokeredMessageProperty.Properties)
                {
                    replyBrokeredMessageProperty.Properties.Add(property);
                }
            }

            // Print the properties of the outbound BrokeredMessageProperty
            replyBrokeredMessageProperty.Properties.Add(Source, CalculatoService);
            Console.WriteLine(ResponseMessagePropertiesHeader);
            Console.WriteLine(line);
            Console.WriteLine(string.Format(PropertyFormat, 
                                            MessageId, 
                               replyBrokeredMessageProperty.MessageId ?? Empty));
            Console.WriteLine(string.Format(PropertyFormat, 
                                            CorrelationId, 
                               replyBrokeredMessageProperty.CorrelationId ?? Empty));
            Console.WriteLine(string.Format(PropertyFormat, 
                                            SessionId, 
                               replyBrokeredMessageProperty.SessionId ?? Empty));
            Console.WriteLine(string.Format(PropertyFormat, 
                                            ReplyTo, 
                               replyBrokeredMessageProperty.ReplyTo ?? Empty));
            Console.WriteLine(string.Format(PropertyFormat, 
                                            Label, 
                               replyBrokeredMessageProperty.Label ?? Empty));
            Console.WriteLine(line);
            if (replyBrokeredMessageProperty.Properties.Count > 0)
            {
                Console.WriteLine(ResponseMessageUserDefinedPropertiesHeader);
                Console.WriteLine(line);
                foreach (var property in replyBrokeredMessageProperty.Properties)
                {
                    Console.WriteLine(string.Format(PropertyFormat, 
                                                    property.Key, 
                                                    property.Value));
                }
                Console.WriteLine(line);
            }

            // Set the outbound context property
            context.SetValue(OutboundBrokeredMessageProperty, 
                             replyBrokeredMessageProperty);
        }
        #endregion
    }
}

Esta actividad expone dos argumentos de entrada y dos argumentos de salida, respectivamente:

  • CalculatorRequest: este InArgument de tipo CalculatorRequest permite que el flujo de trabajo pase la solicitud a la actividad.

  • InboundBrokeredMessageProperty: este InArgument de tipo BrokeredMessageProperty permite pasar como parámetro de entrada el BrokeredMessageProperty extraído del mensaje de solicitud WCF.

  • CalculatorResponse: este OutArgument de tipo CalculatorResponse usa la actividad de código para devolver la respuesta como parámetro de salida al flujo de trabajo.

  • OutboundBrokeredMessageProperty: la actividad de código usa este OutArgument para devolver la BrokeredMessageProperty saliente al flujo de trabajo que utilizará una instancia de BrokeredMessagePropertyActivity para asignar el valor de este parámetro de salida a la BrokeredMessageProperty del mensajes de respuesta WCF.

En pocas palabras, la CalculatorActivity recibe el mensaje entrante y BrokeredMessageProperty como argumentos de entrada, procesa el mensaje de solicitud y genera un mensaje de respuesta y un BrokeredMessageProperty saliente. Al ejecutarse en una aplicación de consola, la actividad realiza el seguimiento de las propiedades de las BrokeredMessageProperty entrante y saliente en la salida estándar.

En esta sección, centraré mi atención en el modo en que el servicio de flujo de trabajo WCF implementa las comunicaciones con la aplicación cliente mediante las colas y los temas de CmdLets. En mi demostración, el servicio de flujo de trabajo WCF se hospeda en una aplicación de consola, pero puede cambiar fácilmente la solución para ejecutar el servicio de flujo de trabajo WCF en una aplicación hospedad en IIS en local o en un rol de Azure en la nube. La tabla siguiente contiene el código que usa la aplicación de consola para inicializar y abrir un objeto WorkflowServiceHost. Concretamente, el extremo HTTP local especificado en el constructor de objeto se puede utilizar para recuperar el WSDL expuesto por el servicio de flujo de trabajo WCF.

Clase de programa


using System;
using System.Xaml;
using System.ServiceModel.Activities;
using Microsoft.WindowsAzure.CAT.Samples.ServiceBusAndWF.Service;

namespace Microsoft.WindowsAzure.CAT.Samples.ServiceBusAndWF.WorkflowConsoleApplication
{

    class Program
    {
        static void Main(string[] args)
        {
            var settings = new XamlXmlReaderSettings()
                               {
                                   LocalAssembly = typeof(Program).Assembly
                               };
            var reader = new XamlXmlReader(@"..\..\CalculatorService.xamlx", settings); 
            var service = (WorkflowService) XamlServices.Load(reader);
            using (var host = new WorkflowServiceHost(service, new Uri("http://localhost:7571")))
            {
                host.Description.Behaviors.Add(new ErrorServiceBehavior());
                host.Open();
                Console.WriteLine("Press [ENTER] to exit");
                Console.ReadLine();
                host.Close();
            }
        }
    }
}

La tabla siguiente contiene el archivo de configuración de la aplicación de consola que desempeña un rol clave en la definición de los extremos de servicio y de cliente WCF que el servicio de flujo de trabajo WCF utiliza para intercambiar los mensajes de respuesta y solicitud con la aplicación cliente a través de las colas y los temas de CmdLets.

Aplicación de consola App.Config


   1:  <?xml version="1.0"?>
   2:  <configuration>
   3:    <system.diagnostics>
   4:      <sources>
   5:        <source name="System.ServiceModel.MessageLogging" switchValue="Warning, ActivityTracing">
   6:          <listeners>
   7:            <add type="System.Diagnostics.DefaultTraceListener" name="Default">
   8:              <filter type="" />
   9:            </add>
  10:            <add name="ServiceModelMessageLoggingListener">
  11:              <filter type="" />
  12:            </add>
  13:          </listeners>
  14:        </source>
  15:      </sources>
  16:      <sharedListeners>
  17:        <add initializeData="C:\WorkflowConsoleApplication.svclog"
  18:             type="System.Diagnostics.XmlWriterTraceListener, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
  19:             name="ServiceModelMessageLoggingListener"
  20:             traceOutputOptions="Timestamp">
  21:          <filter type="" />
  22:        </add>
  23:      </sharedListeners>
  24:    </system.diagnostics>
  25:    <startup>
  26:      <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
  27:    </startup>
  28:    <system.serviceModel>
  29:      <diagnostics>
  30:        <messageLogging logEntireMessage="true"
  31:                        logMalformedMessages="true"
  32:                        logMessagesAtServiceLevel="true"
  33:                        logMessagesAtTransportLevel="true" />
  34:        </diagnostics>
  35:      <behaviors>
  36:        <serviceBehaviors>
  37:          <behavior>
  38:            <serviceMetadata httpGetEnabled="true" />
  39:            <serviceDebug includeExceptionDetailInFaults="true" />
  40:            <useRequestHeadersForMetadataAddress />
  41:            <workflowUnhandledException action="AbandonAndSuspend" />
  42:          </behavior>
  43:        </serviceBehaviors>
  44:        <endpointBehaviors>
  45:          <behavior name="securityBehavior">
  46:            <transportClientEndpointBehavior>
  47:              <tokenProvider>
  48:                <sharedAccessSignature keyName="RootManageSharedAccessKey" key="yourKey" />
  50:              </tokenProvider>
  51:            </transportClientEndpointBehavior>
  52:          </behavior>
  53:        </endpointBehaviors>
  54:      </behaviors>
  55:      <bindings>
  56:        <netMessagingBinding>
  57:          <binding name="netMessagingBinding" 
  58:                   sendTimeout="00:03:00" 
  59:                   receiveTimeout="00:03:00" 
  60:                   openTimeout="00:03:00" 
  61:                   closeTimeout="00:03:00" 
  62:                   sessionIdleTimeout="00:01:00" 
  63:                   prefetchCount="-1">
  64:            <transportSettings batchFlushInterval="00:00:01"/>
  65:          </binding>
  66:        </netMessagingBinding>
  67:      </bindings>
  68:      <client>
  69: <endpoint address="sb://NAMESPACE.servicebus.windows.net/responsequeue" 
  70:                  behaviorConfiguration="securityBehavior" 
  71:                  binding="netMessagingBinding" 
  72:                  bindingConfiguration="netMessagingBinding" 
  73:                  contract="ICalculatorResponse" 
  74:                  name="ResponseQueueClientEndpoint"/>
  75: <endpoint address="sb://NAMESPACE.servicebus.windows.net/responsetopic" 
  76:                  behaviorConfiguration="securityBehavior" 
  77:                  binding="netMessagingBinding" 
  78:                  bindingConfiguration="netMessagingBinding" 
  79:                  contract="ICalculatorResponse" 
  80:                  name="ResponseTopicClientEndpoint"/>
  81:      </client>
  82:      <services>
  83:        <service name="CalculatorService">
  84: <endpoint address="sb://NAMESPACE.servicebus.windows.net/requestqueue" 
  85:                    behaviorConfiguration="securityBehavior" 
  86:                    binding="netMessagingBinding" 
  87:                    bindingConfiguration="netMessagingBinding" 
  88:                    name="RequestQueueServiceEndpoint" 
  89:                    contract="ICalculatorRequest"/>
  90: <endpoint address="sb://NAMESPACE.servicebus.windows.net/requesttopic"
  91:           listenUri=
"sb://NAMESPACE.servicebus.windows.net/requesttopic/Subscriptions/ItalyMilan"
  92:                    behaviorConfiguration="securityBehavior"
  93:                    binding="netMessagingBinding"
  94:                    bindingConfiguration="netMessagingBinding"
  95:                    name="RequestTopicServiceEndpoint"
  96:                    contract="ICalculatorRequest" />
  97:        </service>
  98:      </services>
  99:    </system.serviceModel>
 100:    <startup>
 101:     <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
 102:    </startup>
 103:  </configuration>

A continuación puede encontrar una breve descripción de los elementos y las secciones principales del archivo de configuración. Observe que la definición de los extremos de servicio y de cliente es dual en cuanto a la configuración de los extremos que usa la aplicación cliente.

  • Las líneas [3-33] habilitar el seguimiento, configuran los orígenes de seguimiento para emitir seguimientos y establecen el nivel de seguimiento. Concretamente, la sección de diagnóstico se configura para realizar el seguimiento de los mensajes en el nivel de servicio y de transporte para un archivo de registro que se puede examinar usando la herramienta del visor de seguimiento de servicio (SvcTraceViewer.exe).

  • Las líneas [36-43] especifican el comportamiento de servicio predeterminado que usa el servicio de flujo de trabajo WCF.

  • Las líneas [44-53] contienen la definición del securityBehavior utilizado por los extremos de cliente y de servicio para autenticarse con el servicio Access Control. Concretamente, el TransportClientEndpointBehavior se utiliza para definir las credenciales del secreto compartido.

  • Las líneas [56-66] contienen la configuración del NetMessagingBinding utilizado por los extremos de cliente y de servicio para intercambiar mensajes con CmdLets.

  • Las líneas [69-74] contienen la definición del ResponseQueueClientEndpoint utilizado por el servicio de flujo de trabajo WCF para enviar mensajes de respuesta a requestqueue. La concatenación de la dirección URL del espacio de nombres de servicio y el nombre de la cola proporcionan la dirección del extremo cliente.

  • Las líneas [78-83] contienen la definición del ResponseTopicClientEndpoint utilizado por el servicio de flujo de trabajo WCF para enviar mensajes de respuesta a responsetopic. La concatenación de la dirección URL del espacio de nombres de servicio y el nombre del tema proporcionan la dirección del extremo cliente.

  • Las líneas [83-89] contienen la definición del RequestQueueServiceEndpoint utilizado por el servicio de flujo de trabajo WCF para recibir mensajes de solicitud de requestqueue. La concatenación de la dirección URL del espacio de nombres de servicio y el nombre de la cola proporcionan la dirección del extremo de servicio.

  • Las líneas [90-96] contienen la definición del RequestTopicServiceEndpoint utilizado por la aplicación para recibir mensajes de solicitud de la suscripción ItalyMilan para requesttopic. Cuando se define un extremo de servicio WCF que usa el NetMessagingBinding para recibir los mensajes de una suscripción, tiene que continuar de la forma siguiente:

    • Como valor del atributo address, debe especificar la dirección URL del tema al que la suscripción pertenece. La concatenación de la dirección URL del espacio de nombres de servicio y el nombre del tema proporcionan la dirección del tema.

    • Como valor del atributo listenUri, debe especificar la dirección URL de la suscripción. La concatenación de la dirección URL del tema, la cadena Suscripciones y el nombre de la suscripción definen la dirección URL de la suscripción.

    • Asigne el valor Explicit al atributo listenUriMode. El valor predeterminado de listenUriMode es Explicit, por lo que este valor es opcional.

  • Las líneas [100-102] especifican qué versión de Common Language Runtime admite la aplicación.

Ahora examinemos los pasos necesarios para crear un servicio de flujo de trabajo WCF que reciba una solicitud y envíe una respuesta a través de las colas y los temas de CmdLets. Cuando creé por primera vez el servicio de flujo de trabajo WCF, el flujo de trabajo solo contenía una actividad Sequence con una actividad Receive seguida de una actividad SendReply, como se muestra en la ilustración siguiente.

Temas-de-colas-del-bus-de-servicio-Servicio-de-flujo-de-trabajo-de-WCF12

En primer lugar, hice clic en la superficie del flujo de trabajo y asigné la cadena CalculatorService como un valor a ambas propiedades, ConfigurationName y Name, de WorkflowService, como se muestra en la imagen siguiente. Concretamente, la propiedad ConfigurationName indica que el nombre de configuración del servicio de flujo de trabajo y su valor deben ser iguales que el valor del atributo name del elemento service del archivo de configuración.

Temas-de-colas-del-bus-de-servicio-Servicio-de-flujo-de-trabajo-de-WCF13

A continuación, seleccioné la actividad Sequential, hice clic en el botón Variables para mostrar el editor correspondiente y creé las variables siguientes:

  • CalculatorRequest: esta variable es de tipo CalculatorRequest y, como el nombre sugiere, contiene el cuerpo del mensaje de solicitud. La actividad Receive, que se usa para recibir el mensaje de solicitud de la suscripción requestqueue o ItalyMilan de requesttopic, establece su valor.

  • CalculatorResponse: esta variable es de tipo CalculatorResponse y contiene el cuerpo del mensaje de respuesta devuelto a la aplicación cliente. El valor se establece mediante una actividad personalizada que procesa la solicitud y genera una respuesta.

  • InboundBrokeredMessageProperty: esta variable contiene la BrokeredMessageProperty del mensaje de solicitud. El valor se establece mediante una instancia BrokeredMessagePropertyActivity que se ajuste a las lecturas de la actividad y Recibir el BrokeredMessageProperty propiedades de mensaje de solicitud de WCF.

  • OutboundBrokeredMessageProperty: esta variable contiene la BrokeredMessageProperty del mensaje de respuesta. El valor se establece con una actividad personalizada que procesa la solicitud y genera una respuesta. Una BrokeredMessagePropertyActivity se utiliza para ajustar la actividad Send y asignar el valor de esta variable a la BrokeredMessageProperty del mensaje de respuesta WCF.

Temas-de-colas-del-bus-de-servicio-Servicio-de-flujo-de-trabajo-de-WCF14

A continuación, agregué una actividad TryCatch al flujo de trabajo y ajusté la actividad Receive a una instancia de la BrokeredMessagePropertyActivity, como se muestra en la imagen siguiente.

Temas-de-colas-del-bus-de-servicio-Servicio-de-flujo-de-trabajo-de-WCF15

Después, hice clic en la BrokeredMessagePropertyActivity y asigné la variable inboundBrokeredMessageProperty a su propiedad BrokeredMessageProperty, como se muestra en la imagen siguiente.

Temas-de-colas-del-bus-de-servicio-Servicio-de-flujo-de-trabajo-de-WCF16

A continuación, seleccioné la actividad Receive y configuré sus propiedades para recibir los mensajes de solicitud de requestqueue y de la suscripción ItalyMilan del requesttopic usando, respectivamente, los extremos de servicio RequestQueueServiceEndpoint y RequestTopicServiceEndpoint definidos en el archivo de configuración.

Temas-de-colas-del-bus-de-servicio-Servicio-de-flujo-de-trabajo-de-WCF17

Concretamente, utilicé la propiedad ServiceContractName de la actividad de recepción para especificar el espacio de nombres de destino y el nombre de contrato para el extremo de servicio y usé la propiedad Action para especificar el encabezado de acción de la solicitud de mensaje como se especifica en el contrato de servicio ICalculatorRequest.

A continuación, agregué una instancia de CalculatorActivity al servicio de flujo de trabajo WCF bajo BrokeredMessagePropertyActivity y configuré sus propiedades como se muestra en la imagen siguiente.

Temas-de-colas-del-bus-de-servicio-Servicio-de-flujo-de-trabajo-de-WCF18

Después, agregué una actividad If al flujo de trabajo debajo de CalculatorActivity y configuré su propiedad Condition de la forma siguiente:

Not String.IsNullOrEmpty(inboundBrokeredMessageProperty.ReplyTo) And 
inboundBrokeredMessageProperty.ReplyTo.ToLower().Contains("topic")

Después, creé una instancia de BrokeredMessagePropertyActivity en ambas bifurcaciones de la actividad If y agregué una actividad Send a cada una, como se muestra en la imagen siguiente.

Temas-de-colas-del-bus-de-servicio-Servicio-de-flujo-de-trabajo-de-WCF19

De esta manera, si la dirección de respuesta especificada en la propiedad ReplyTo de la BrokeredMessageProperty entrante contiene la cadena “topic”, la respuesta se envía a responsetopic y, en caso contrario, la respuesta se envía a responsequeue.

En ambos casos, una instancia de BrokeredMessagePropertyActivity (identificada mediante el nombre para mostrar Set BrokeredMessage) se utiliza para ajustar la actividad Send y asignar la BrokeredMessageProperty saliente a la colección de propiedades del mensajes de respuesta WCF. Para realizar esta operación, asigné el valor de la variable outboundBrokeredMessageProperty a la propiedad BrokeredMessageProperty de ambas instancias de BrokeredMessagePropertyActivity, como se muestra en la imagen a continuación.

Temas-de-colas-del-bus-de-servicio-Servicio-de-flujo-de-trabajo-de-WCF20

A continuación, seleccioné la actividad Send en la bifurcación Then y configuré su propiedad de la forma siguiente para enviar el mensaje de respuesta a responsetopic mediante el ResponseTopicClientEndpoint definido en el archivo de configuración.

Temas-de-colas-del-bus-de-servicio-Servicio-de-flujo-de-trabajo-de-WCF21

Concretamente, utilicé la propiedad ServiceContractName de la actividad Send para especificar el nombre de contrato y el espacio de nombres de destino para el extremo cliente, la propiedad Action para especificar el encabezado de acción del mensaje de respuesta que se especifica en el contrato de servicio ICalculatorResponse, EndpointConfigurationName para indicar el nombre del extremo de cliente definido en el archivo de configuración.

Igualmente, configuré la actividad Send en la bifurcación Else, como se muestra en la ilustración siguiente.

Temas-de-colas-del-bus-de-servicio-Servicio-de-flujo-de-trabajo-de-WCF22

En la siguiente imagen se muestra el flujo de trabajo entero:

Temas-de-colas-del-bus-de-servicio-Servicio-de-flujo-de-trabajo-de-WCF23

Suponiendo que ha configurado correctamente la solución, puede seguir de la forma siguiente para probarla.

  • Para enviar un mensaje de solicitud al servicio de flujo de trabajo WCF mediante requestqueue, seleccione el botón de opción Cola en el grupo Métodos de solicitud.

  • Para enviar un mensaje de solicitud al servicio de flujo de trabajo WCF mediante requesttopic, seleccione el botón de opción Tema en el grupo Métodos de solicitud.

  • Para pedir al servicio de flujo de trabajo WCF que envíe el mensaje de respuesta a responsequeue, seleccione el botón de opción Cola en el grupo Métodos de respuesta.

  • Para pedir al servicio de flujo de trabajo WCF que envíe el mensaje de respuesta a responsetopic, seleccione el botón de opción Tema en el grupo Métodos de respuesta.

En la ilustración siguiente se muestra la combinación más interesante:

  • El cliente envía un mensaje de solicitud a requesttopic.

  • El servicio del flujo de trabajo WCF lee la solicitud de la suscripción ItalyMilan para requesttopic y envía la respuesta a responsetopic.

  • La aplicación cliente recibe el mensaje de respuesta de la suscripción ItalyMilan definida en responsetopic.

La siguiente imagen muestra la información registrada por el servicio de flujo de trabajo WCF en la salida estándar de la aplicación de consola de host:

Temas-de-colas-del-bus-de-servicio-Servicio-de-flujo-de-trabajo-de-WCF24

La siguiente imagen muestra la información registrada por la aplicación cliente durante la llamada:

Temas-de-colas-del-bus-de-servicio-Servicio-de-flujo-de-trabajo-de-WCF25

Concretamente, tenga en cuenta lo siguiente:

  1. Se envía el mensaje de solicitud a requesttopic.

  2. El mensaje de respuesta se recibe de responsetopic.

  3. El CorrelationId del mensaje de respuesta es igual que el MessageId del mensaje de solicitud.

  4. El servicio de flujo de trabajo WCF copió la propiedad Label del mensaje de solicitud para la propiedad Label del mensaje de respuesta.

  5. El servicio de flujo de trabajo WCF copió todas las propiedades definidas por el usuario del mensaje de solicitud al mensaje de respuesta.

  6. El servicio de flujo de trabajo WCF agregó la propiedad definida por el usuario Source a la respuesta.

  7. La acción de la regla definida en la suscripción ItalyMilan agregó la propiedad Area.

En este artículo hemos visto cómo integrar un servicio de flujo de trabajo WCF con la mensajería asíncrona de CmdLets y cómo lograr una interoperabilidad completa entre estas dos tecnologías simplemente usando sus características nativas y una actividad personalizada para administrar BrokeredMessageProperty. Espero conocer sus opiniones y, mientras tanto, puede descargar el código complementario de este artículo desde la Galería de código de MSDN.

Mostrar:
© 2015 Microsoft