VENTES: 1-800-867-1389

Intégrer un service de flux de travail WCF aux files d'attente et aux sujets Service Bus

Mis à jour: mars 2015

Auteur : Paolo Salvatori

Réviseurs : Ralph Squillace, Sidney Higa

Ce document fournit des conseils concernant l'intégration d'un service de workflow WCF aux rubriques et files d'attente de Microsoft Azure Service Bus. Il vous guidera dans la création d'un service de workflow WCF qui utilise des files d'attente et rubriques pour échanger des messages avec une application cliente. En complément de ce guide, consultez l'article de Roman Kiss, Utilisation de la messagerie d'Azure Service Bus, qui propose une solution plus sophistiquée au même problème, ainsi qu'une série d'activités réutilisables dans un service de workflow WCF pour interagir avec des files d'attente et rubriques Service Bus.

Pour plus d'informations sur Microsoft Azure Service Bus, consultez les ressources suivantes :

La figure suivante illustre l'architecture de niveau supérieur de la démonstration :

Bus de service-Files d'attente-Rubriques-WCF-Flux de travail-Service4

  1. Une application cliente utilise un proxy WCF et NetMessagingBinding pour envoyer un message de demande à requestqueue ou à requesttopic.

  2. Le service de flux de travail WCF dans une application console ou IIS reçoit le message de demande de requestqueue ou de l'abonnement ItalyMilan défini sur requesttopic.

    Bus de service-Files d'attente-Rubriques-WCF-Flux de travail-Service5
  3. Le service de flux de travail WCF, représenté dans la figure ci-dessus, effectue les actions suivantes :

    • L'activité personnalisée BrokeredMessagePropertyActivity (identifiée par le nom complet Get BrokeredMessage ) lit l'objet BrokeredMessageProperty du message entrant et affecte sa valeur à une variable de flux de travail définie à l'extérieur de l'activité Sequential.

    • L'activité Receive récupère le message de requestqueue ou de l'abonnement ItalyMilan de requesttopic.

    • L'activité CalculatorActivity personnalisée reçoit le message entrant et BrokeredMessageProperty en tant qu'arguments d'entrée, traite le message de demande et génère un message de réponse et un objet BrokeredMessageProperty sortant. Dans une application console, l'activité trace les propriétés de l'objet BrokeredMessageProperty entrant et sortant dans la sortie standard.

    • L'activité If lit l'adresse contenue dans la propriété ReplyTo de l'objet BrokeredMessageProperty entrant.

      • Si la chaîne contient le mot « topic », la réponse est envoyée à responsetopic.

      • Sinon, la réponse est envoyée à responsequeue.

    • Dans les deux cas, une instance de BrokeredMessagePropertyActivity (identifiée par le nom complet Set BrokeredMessage) permet d'encapsuler l'activité Send et d'assigner l'objet BrokeredMessageProperty sortant à la collection de propriétés du message de réponse WCF.

  4. Le service de flux de travail WCF écrit le message de réponse dans responsequeue ou responsetopic.

  5. L'application cliente utilise un service WCF avec deux points de terminaison distincts pour extraire le message de réponse de responsequeue ou de responsetopic. Dans un environnement comportant plusieurs applications clientes, chacune d'elles doit utiliser une file d'attente ou un abonnement distinct pour recevoir des messages de réponse de BizTalk. Ce thème sera approfondi plus loin dans cet article.

Maintenant que nous avons décrit l'architecture globale de la démonstration, nous pouvons analyser en détail les principaux composants de la solution.

La première étape pour configurer correctement l'environnement consiste à créer les entités de messagerie utilisées par la démonstration. Commencez par configurer un nouvel espace de noms Service Bus, ou modifier un espace de noms existant pour inclure Service Bus. Vous pouvez accomplir cette tâche dans le Portail de gestion Azure, respectivement, en cliquant sur le bouton Nouveau ou sur le bouton Modifier.

L'étape suivante consiste à créer les entités de file d'attente, de sujet et d'abonnement nécessaires à la démonstration. Comme mentionné dans l'introduction, vous pouvez exécuter cette tâche de nombreuses manières. La façon la plus simple consiste à utiliser le Portail de gestion Azure et les boutons de la commande Gérer les entités.

Vous pouvez utiliser l'arborescence de navigation illustrée au point 2 pour sélectionner une entité existante et afficher ses propriétés dans la barre verticale mise en surbrillance au point 3. Pour supprimer l'entité sélectionnée, cliquez sur le bouton Supprimer dans la barre de commandes Gérer les entités.

noteRemarque
Le portail de gestion Azure permet de gérer aisément les entités de messagerie dans un espace de noms Service Bus spécifique. Toutefois, du moins à l'heure actuelle, les opérations qu'un développeur ou un administrateur système peut exécuter à l'aide de cette interface utilisateur sont assez limitées. Par exemple, le portail de gestion Azure permet aux utilisateurs de créer des files d'attente, des sujets et des abonnements, et de définir leurs propriétés, mais pas de créer ni d'afficher les règles d'un abonnement existant. À ce jour, vous pouvez accomplir ces tâches uniquement à l'aide de l'API de messagerie .NET. Notamment, pour ajouter une règle à un abonnement existant, vous pouvez utiliser la méthode AddRule(String, Filter) ou la méthode AddRule (RuleDescription) exposées par la classe SubscriptionClient, tandis que pour énumérer les règles d'un abonnement existant, vous pouvez utiliser la méthode GetRules de la classe NamespaceManager. En outre, le portail de gestion Azure ne permet pas d'effectuer les opérations suivantes :

  1. Visualiser correctement les entités de façon hiérarchique. En fait, le portail de gestion Azure affiche les files d'attente, les sujets et les abonnements dans une arborescence plate. Toutefois, vous pouvez organiser les entités de messagerie dans une structure hiérarchique en spécifiant simplement leur nom sous la forme d'un chemin d'accès absolu, composé de plusieurs segments de chemin d'accès, par exemple crm/prod/queues/orderqueue.

  2. Exporter les entités de messagerie contenues dans un espace de noms Service Bus spécifique vers un fichier de liaison XML (BizTalk Server). En revanche, l'explorateur de Service Bus permet la sélection et l'exportation :

    1. d'entités individuelles ;

    2. d'entités par type (files d'attente ou rubriques) ;

    3. d'entités contenues dans un certain chemin d'accès (par exemple crm/prod/queues) ;

    4. de toutes les entités dans un espace de noms donné.

  3. Importer des files d'attente, des rubriques et des abonnements d'un fichier XML dans un espace de noms existant. L'explorateur de Service Bus permet d'exporter des entités dans un fichier XML et de les réimporter sur la même instance ou une autre instance d'espace de noms Service Bus. Cette fonctionnalité est pratique pour la sauvegarde et la restauration d'un espace de noms, ou pour transférer simplement les files d'attente et les rubriques d'un espace de noms de test vers un espace de noms en production.

L'explorateur Service Bus permet à l'utilisateur de créer, supprimer et tester les files d'attente, les sujets, les abonnements et les règles ; c'est donc l'assistant parfait du portail de gestion Azure officiel.

Par souci pratique, j'ai créé une application console appelée Approvisionnement qui utilise la fonctionnalité fournie par la classe NamespaceManager pour créer les files d'attente, les rubriques et les abonnements nécessaires à la solution. Au démarrage, les applications console demandent les informations d'identification de l'espace de noms du service. Celles-ci permettent l'authentification au près du service Access Control, et d'obtenir un jeton d'accès prouvant à l'infrastructure Service Bus que l'application est autorisée à déployer de nouvelles entités de messagerie. Puis, l'application demande la valeur à affecter aux propriétés des entités à créer, par exemple EnabledBatchedOperations et EnableDeadLetteringOnMessageExpiration pour les files d'attente. L'application de déploiement crée les entités suivantes dans l'espace de noms Service Bus spécifié :

  1. Une file d'attente nommée requestqueue utilisé par l'application cliente pour envoyer des messages de demande à BizTalk Server.

  2. Une file d'attente nommée responsequeue utilisé par BizTalk Server pour envoyer des messages de réponse à l'application cliente.

  3. Une rubrique nommée requesttopic utilisée par l'application cliente pour envoyer des messages de demande à BizTalk Server.

  4. Une rubrique nommée responsetopic utilisée par BizTalk Server pour envoyer des messages de réponse à l'application cliente.

  5. Un abonnement appelé ItalyMilan pour requesttopic. Ce dernier est utilisé par BizTalk Server pour recevoir des messages de demande de requesttopic. L'abonnement en question possède une seule règle, définie comme suit :

    1. Filtre : Country='Italy' et City='Milan'

    2. Action : Set Area='Western Europe'

  6. Un abonnement appelé ItalyMilan pour responsetopic. Ce dernier est utilisé par l'application cliente pour recevoir des messages de réponse de responsetopic. L'abonnement en question possède une seule règle, définie comme suit :

    1. Filtre : Country='Italy' et City='Milan'

    2. Action : Set Area='Western Europe'

La figure suivante illustre les résultats de l'application console Approvisionnement.

Bus de service-Files d'attente-Rubriques-WCF-Flux de travail-Service8

Le tableau suivant contient le code de l'application Approvisionnement :

#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);
            }
        }
    }
}

noteRemarque
Je n'ai pas testé toutes les combinaisons possibles des propriétés des files d'attente et des sujets, par conséquent, la démonstration peut ne pas fonctionner comme prévu avec certaines configurations.

J'ai commencé à définir les contrats de données pour les messages de demande et de réponse. Les contrats de données permettent de mapper des types CLR .NET définis dans le code et des schémas XML (XSD) défini par l'organisation W3C (www.w3c.org). Les contrats de données sont publiés dans les métadonnées du service, et permettent aux clients de convertir la représentation neutre et agnostique des types de données en leurs représentations natives. Pour plus d'informations sur les données et les contrats, vous pouvez consulter les articles suivants :

Dans ma solution, j'ai créé un projet appelé DataContracts pour définir des classes qui représentent, respectivement, les messages de demande et de réponse. Pour plus de commodité, j'ai inclus leur code ci-dessous.

Classe CalculatorRequest

[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
}

Classe CalculatorResponse

[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
}

L'étape suivante consiste à définir les Contrats de service utilisés par l'application cliente pour échanger des messages avec Service Bus. À cette fin, j'ai créé dans ma solution un projet nommé ServiceContracts, et j'ai défini deux interfaces de contrat de service utilisées par l'application cliente pour échanger des messages avec les entités de messagerie Service Bus. En réalité, j'ai créé deux versions différentes du contrat de service utilisé pour recevoir des messages de réponse :

  • L'interface ICalculatorResponse est destinée à recevoir des messages de réponse d'une file d'attente ou d'un abonnement sans session.

  • L'interface ICalculatorResponseSessionful hérite du contrat de service ICalculatorResponse et est marquée avec l'attribut [ServiceContract(SessionMode = SessionMode.Required)]. Ce contrat de service est destiné à recevoir des messages de réponse d'une file d'attente ou d'un abonnement sans session.

Notez que les méthodes définies par tous les contrats sont monodirectionnelles. Il s'agit d'une condition obligatoire.

Interfaces ICalculatorRequest, ICalculatorResponse, ICalculatorResponseSessionful

#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
    {
    }
}

Nous pouvons maintenant examiner le code de l'application cliente.

Étant donné que l'application Windows Forms échange des messages avec le service de worklow WCF sous-jacent de façon asynchrone via les entités de messagerie Service Bus, elle agit en même temps comme un client et une application de service. Windows Forms utilise WCF et NetMessagingBinding pour effectuer les actions suivantes :

  1. Envoyer des messages de demande à requestqueue.

  2. Envoyer des messages de demande à requesttopic.

  3. Recevoir des messages de réponse de responsequeue.

  4. Recevoir des messages de réponse de l'abonnement ItalyMilan de responsetopic.

Commençons par examiner le fichier de configuration de l'application cliente, qui joue un rôle central dans la définition du client WCF, et des points de terminaison du service utilisés pour communiquer avec Service Bus.

Fichier App.Config de l'application cliente


   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>

Vous trouverez ci-dessous sous une brève description des éléments et des sections principales du fichier de configuration :

  • Les lignes [3-32] définissent un écouteur de suivi personnalisé appelé LogTraceListener, utilisé par ResponseHandlerService pour écrire le message de réponse dans le contrôle du journal de l'application Windows Forms.

  • Dans les lignes [34-36], la section de démarrage spécifie les versions du CLR (Common Langage Runtime) prises en charge par l'application.

  • Les lignes [46-53] contiennent la définition du securityBehavior utilisé par le client et le point de terminaison du service pour s'authentifier au moyen du service Access Control. Notamment, TransportClientEndpointBehavior est utilisé pour définir les informations d'identification secrètes partagées. Pour plus d'informations sur la manière de récupérer les informations d'identification dans le Portail de gestion Azure, consultez la zone ci-dessous.

  • Les lignes [57-67] contiennent la configuration de la liaison NetMessagingBinding utilisée par le client et les points de terminaison du service pour échanger des messages avec Service Bus.

  • Les lignes [71-76] contiennent la définition du requestQueueClientEndpoint utilisé par l'application pour envoyer des messages de demande à requestqueue. L'attribut address du point de terminaison du client est fourni par la concaténation de l'URL de l'espace de noms du service et le nom de la file d'attente.

  • Les lignes [78-83] contiennent la définition du requestTopicClientEndpoint utilisé par l'application pour envoyer des messages de demande à requesttopic. L'attribut address du point de terminaison du client est fourni par la concaténation de l'URL de l'espace de noms du service et le nom du sujet.

  • Les lignes [87-92] contiennent la définition du responseQueueServiceEndpoint utilisé par l'application pour recevoir les messages de réponse de responsequeue. L'attribut address du point de terminaison du service est fourni par la concaténation de l'URL de l'espace de noms du service et le nom de la file d'attente.

  • Les lignes [93-99] contiennent la définition du responseSubscriptionServiceEndpoint utilisé par l'application pour recevoir des messages de réponse de l'abonnement ItalyMilan pour responsetopic. Lorsque vous définissez un point de terminaison de service WCF qui utilise NetMessagingBinding pour recevoir les messages d'un abonnement, vous devez procéder comme suit (pour plus d'informations sur cette opération, consultez la zone ci-dessous) :

    • Comme valeur de l'attribut address, vous devez spécifier l'URL du sujet auquel l'abonnement appartient. L'URL du sujet est fournie par la concaténation de l'URL de l'espace de noms du service et le nom du sujet.

    • Comme valeur de l'attribut listenUri, vous devez spécifier l'URL de l'abonnement. L'URL de l'abonnement est définie par la concaténation de l'URL du sujet, la chaîne /Subscriptions/ et l'espace de noms de l'abonnement.

    • Affectez la valeur Explicit à l'attribut listenUriMode. La valeur par défaut de listenUriMode est Explicit, par conséquent, ce paramètre est facultatif.

noteRemarque
Lorsque vous configurez un point de terminaison du service WCF pour consommer des messages d'une file d'attente ou d'un abonnement de session, le contrat de service doit prendre en charge les sessions. Par conséquent, dans notre exemple, lorsque vous configurez les points de terminaison responseQueueServiceEndpoint ou responseSubscriptionServiceEndpoint pour recevoir des messages, respectivement, d'une file d'attente et d'un abonnement de session, vous devez remplacer le contrat de service ICalculatorResponse par l'interface du contrat ICalculatorResponseSessionful de session. Pour plus d'informations, consultez la section Contrats de service plus loin dans cet article.

noteRemarque
Service Bus prend en charge trois types de modèles d'informations d'identification : SAML, Secret partagé et Jeton web simple, mais cette version de l'Explorateur Service Bus prend en charge uniquement les informations d'identification de type Secret partagé. Toutefois, vous pouvez facilement étendre mon code pour prendre en charge d'autres modèles d'identification. Vous pouvez récupérer la clé d'émetteur-secret depuis le Portail de gestion Azure en cliquant sur le bouton Affichage après avoir sélectionné un espace de noms spécifique dans la section Service Bus.

Une boîte de dialogue modale s'affiche, et vous permet de récupérer la clé en cliquant sur Copier dans le Presse-papiers surligné en rouge.

noteRemarque
Par convention, le nom de l'Émetteur par défaut est toujours owner.

noteRemarque
Lorsque vous définissez un point de terminaison du service WCF qui utilise la liaison NetMessagingBinding pour recevoir les messages d'un abonnement, si vous faites l'erreur d'affecter l'URL de l'abonnement à l'attribut d'adresse du point de terminaison du service (comme indiqué dans la configuration ci-dessous), au moment de l'exécution, une erreur FaultException semblable à la suivante se produit :

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."}

Configuration incorrecte


<?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>

L'erreur est due au fait que l'en-tête To WS-Addressing du message contient l'adresse du sujet et non l'adresse de l'abonnement :


<: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>

Pour configurer correctement le point de terminaison du service de façon à recevoir les messages d'un abonnement, vous devez procéder comme suit :

  • Comme valeur de l'attribut address, vous devez spécifier l'URL du sujet auquel l'abonnement appartient. L'URL du sujet est fournie par la concaténation de l'URL de l'espace de noms du service et le nom du sujet.

  • Comme valeur de l'attribut listenUri, vous devez spécifier l'URL de l'abonnement. L'URL de l'abonnement est définie par la concaténation de l'URL du sujet, la chaîne /Subscriptions/ et l'espace de noms de l'abonnement.

  • Affectez la valeur Explicit à l'attribut listenUriMode. La valeur par défaut de listenUriMode est Explicit, par conséquent, ce paramètre est facultatif.

Consultez la page suivante sur MSDN pour obtenir une description des attributs address, listenUri et listenUriMode.

Configuration correcte


<?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>

Pour effectuer la même tâche via l'API, vous devez définir correctement la valeur des propriétés Address, ListenUri et ListenUriMode de votre instance ServiceEndpoint comme indiqué dans cette remarque.

Le tableau suivant contient le code utilisé par l'application cliente pour démarrer le ResponseHandlerService utilisé pour lire les messages de réponse de responsequeue et l'abonnement ItalyMilan de responsetopic. Nous examinerons le code du service dans la section suivante.


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);
    }
}

L'image suivante montre l'interface utilisateur de l'application cliente :

Bus de service-Files d'attente-Rubriques-WCF-Flux de travail-Service11

Les cases d'option contenues dans le groupe Méthode de demande permettent d'envoyer le message de demande à requestqueue ou à requesttopic, tandis que les cases d'option contenues dans le groupe Méthode de réponse permettent de recevoir la réponse de responsequeue ou de l'abonnement ItalyMilan de responsetopic. Pour communiquer ce choix à l'application BizTalk sous-jacente, l'application utilise un objet BrokeredMessageProperty pour affecter la valeur des champs privés responseQueueUri ou responseTopicUri à la propriété ReplyTo. Le tableau suivant contient le code de la méthode utilisée par l'application cliente pour envoyer des messages à Service Bus. Pour plus de commodité, des commentaires ont été ajoutés au code pour en faciliter la compréhension.


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;
    }
}

Le tableau suivant contient le code du service WCF utilisé par l'application cliente pour récupérer et enregistrer les messages de réponse de responsequeue et de l'abonnement ItalyMilan de responsetopic. Pour obtenir ce résultat, le service expose deux points de terminaison différents qui utilisent chacun la liaison NetMessagingBinding et reçoivent des messages de l'une des deux files d'attente. En effet, chaque abonnement peut être considéré comme une file d'attente virtuelle, obtenant des copies des messages publiés dans la rubrique à laquelle ils appartiennent. Le tableau ci-dessous montre le code de la classe de ResponseHandlerService. Comme vous pouvez le remarquer, le service obtient un BrokeredMessageProperty de la collection Properties du message WCF entrant et utilise cet objet pour accéder aux propriétés du message de réponse. Étant donné que dans le contrat de service ICalculatorResponse la méthode ReceiveResponse est décorée avec [ReceiveContextEnabled(ManualControl = true)], l'accusé de réception doit être explicitement signalé par la méthode du service. Cela exige que le service appelle explicitement la méthode ReceiveContext.Complete pour valider l'opération de réception. En fait, comme mentionné au début de cet article, lorsque la propriété ManualControl a la valeur true, le message reçu du canal est remis à l'opération de service avec un verrou. L'implémentation du service doit alors appeler Complete(TimeSpan) ou Abandon(TimeSpan) pour signaler la fin de la réception du message. Faute de ces appels, le verrou est maintenu sur le message jusqu'à son expiration. Une fois le verrou libéré (soit par appel à Abandon(TimeSpan), soit par expiration) le message est redistribué du canal vers le service. L'appel à Complete(TimeSpan) marque le message comme correctement reçu.

Classe 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
 }

Les services de flux de travail WCF fournissent un environnement productif pour créer des opérations ou des services durables. Les services de flux de travail sont implémentés à l'aide des activités WF, pouvant utiliser WCF pour envoyer et recevoir des données. La création détaillée d'un service de flux de travail WCF n'est pas abordée dans cet article. Pour plus d'informations sur les services de flux de travail WCF, consultez la documentation suivante :

WF 4.0 comporte des opérations de messagerie qui permettent aux développeurs d'exposer ou de consommer des services WCF de façon simple et souple. Notamment, les opérations de messagerie permettent aux flux de travail d'envoyer des données à d'autres systèmes (Send, SendReply), et de recevoir des données d'autres systèmes (Receive, ReceiveReply) avec WCF. Toutefois, ces opérations n'exploitent pas la totalité des capacités de WCF. Par exemple, les opérations de messagerie ne permettent pas d'accéder à OperationContext, qui peut être utilisé pour exécuter l'opération suivante :

  • Du côté envoi, OperationContext permet d'inclure des en-têtes de message supplémentaires dans l'enveloppe SOAP ou d'ajouter des propriétés dans le message sortant.

  • Du côté réception, OperationContext peut être utilisé pour extraire des propriétés et des informations de sécurité du message entrant.

Comme nous l'avons vu dans la première partie de l'article, lorsqu'une application utilise WCF et la liaison NetMessagingBinding pour envoyer un message à une file d'attente ou à un sujet, le message est encapsulé dans une enveloppe SOAP, puis encodé. Pour définir les propriétés BrokeredMessage, vous devez créer un objet BrokeredMessageProperty, lui attribuer les propriétés, et l'ajouter à la collection Properties du Message WCF. Par conséquent, pour extraire un objet BrokeredMessageProperty d'un message entrant ou ajouter un BrokeredMessageProperty à la collection Properties d'un message WCF sortant, nous devons étendre la fonctionnalité prête à l'emploi fournie par les opérations de messagerie. Heureusement, WF 4.0 permet d'étendre le comportement d'exécution des opérations de messagerie à l'aide de IReceiveMessageCallback et de ISendMessageCallback. Notamment :

  • L'interface IReceiveMessageCallback implémente un rappel exécuté quand un message de service est reçu par l'activité Receive.

  • ISendMessageCallback.interface implémente un rappel appelé juste avant qu'un message ne soit envoyé par l'activité Send.

Dans ma démonstration, j'ai exploité ces points d'extensibilité pour créer une opération NativeActivity personnalisée appelée BrokeredMessagePropertyActivity qui permet :

  • d'obtenir un BrokeredMessageProperty des propriétés d'un message WCF entrant ;

  • de définir un BrokeredMessageProperty pour un message WCF sortant.

Roman Kiss, dans son article a suivi la même approche et a créé une activité plus sophistiquée qui expose une propriété pour chaque propriété exposée par la classe BrokeredMessageProperty. Je vous recommande vivement de lire son article, car il explique comment implémenter autrement la même technique que je décris dans cet article.

Pour plus de commodité, j'ai inclus le code de l'objet BrokeredMessagePropertyActivity dans le tableau ci-dessous :

Classe 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  
}

Classe 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
}

BrokeredMessagePropertyActivity peut être utilisé pour encapsuler les activités Receive ou Send. Cela permet d'accéder automatiquement au BrokeredMessageProperty, qui est utilisé pour lire et écrire des propriétés explicites définies par l'utilisateur d'un BrokeredMessage.

Pour développer la requête entrante et générer une réponse, j'ai créé une activité de code personnalisé appelée CalculatorActivity dont le code est présenté dans le tableau suivant.

Classe 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
    }
}

Cette activité expose deux arguments d'entrée et deux arguments de sortie, respectivement :

  • CalculatorRequest : cet InArgument de type CalculatorRequest permet au workflow de transmettre la demande à l'activité.

  • InboundBrokeredMessageProperty : cet InArgument de type BrokeredMessageProperty permet de passer en tant que paramètre d'entrée le BrokeredMessageProperty extrait du message de demande WCF.

  • CalculatorResponse : cet OutArgument de type CalculatorResponse est utilisé par l'activité de code pour renvoyer la réponse en tant que paramètre de sortie au workflow.

  • OutboundBrokeredMessageProperty : cet OutArgument est utilisé par l'activité de code pour renvoyer le BrokeredMessageProperty sortant au workflow qui utilisera une instance de BrokeredMessagePropertyActivity pour affecter la valeur de ce paramètre de sortie au BrokeredMessageProperty du message de réponse WCF.

Pour résumer, CalculatorActivity reçoit le message entrant et BrokeredMessageProperty en tant qu'arguments d'entrée, traite le message de demande et génère un message de réponse et un objet BrokeredMessageProperty sortant. Dans une application console, l'activité trace les propriétés de l'objet BrokeredMessageProperty entrant et sortant dans la sortie standard.

Dans cette section, nous allons nous concentrer sur la façon dont le service de workflow WCF implémente les communications avec l'application cliente au moyen de files d'attente et de rubriques Service Bus. Dans ma démonstration, le service de flux de travail WCF est hébergé dans une application console, mais vous pouvez modifier facilement la solution pour l'exécuter dans l'application hébergée par IIS sur site, ou dans un rôle Azure dans le cloud. Le tableau suivant contient le code utilisé par l'application console pour initialiser et ouvrir un objet WorkflowServiceHost. Par exemple, le point de terminaison HTTP local spécifié dans le constructeur d'objet peut être utilisé pour extraire le WSDL exposé par le service de flux de travail WCF.

Classe de programme


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();
            }
        }
    }
}

Le tableau suivant contient le fichier de configuration de l'application console, qui joue un rôle clé dans la définition du client WCF et des points de terminaison du service utilisés par le service de workflow WCF pour échanger des messages de demande et de réponse avec l'application cliente, via des files d'attente et rubriques Service Bus.

Fichier App.Config de l'application console


   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>

Vous trouverez ci-dessous sous une brève description des éléments et des sections principales du fichier de configuration. Notez que la définition du client et des points de terminaison du service est double par rapport à la configuration du client et des points de terminaison du service utilisés par l'application cliente.

  • Les lignes [3-33] activent le suivi, et configurent les sources pour générer les suivis et en définir le niveau. Notamment, la section de diagnostic est configurée pour suivre les messages au niveau du service et du transport dans un fichier journal qui peut être passé en revue à l'aide de l'outil Service Trace Viewer (SvcTraceViewer.exe).

  • Les lignes [36-43] spécifient le comportement par défaut du service de flux de travail WCF.

  • Les lignes [44-53] contiennent la définition du securityBehavior utilisé par le client et le point de terminaison du service pour s'authentifier au moyen du service Access Control. Notamment, TransportClientEndpointBehavior est utilisé pour définir les informations d'identification secrètes partagées.

  • Les lignes [56-66] contiennent la configuration de la liaison NetMessagingBinding utilisée par le client et les points de terminaison du service pour échanger des messages avec Service Bus.

  • Les lignes [69-74] contiennent la définition du ResponseQueueClientEndpoint utilisé par le service de flux de travail WCF pour envoyer des messages de réponse à responsequeue. L'attribut address du point de terminaison du client est fourni par la concaténation de l'URL de l'espace de noms du service et le nom de la file d'attente.

  • Les lignes [78-83] contiennent la définition du ResponseTopicClientEndpoint utilisé par le service de flux de travail WCF pour envoyer des messages de réponse à responsetopic. L'attribut address du point de terminaison du client est fourni par la concaténation de l'URL de l'espace de noms du service et le nom du sujet.

  • Les lignes [83-89] contiennent la définition du RequestQueueServiceEndpoint utilisé par le service de flux de travail WCF pour recevoir les messages de demande de requestqueue. L'attribut address du point de terminaison du service est fourni par la concaténation de l'URL de l'espace de noms du service et le nom de la file d'attente.

  • Les lignes [90-96] contiennent la définition du RequestTopicServiceEndpoint utilisé par l'application pour recevoir des messages de demande de l'abonnement ItalyMilan pour requesttopic. Lorsque vous définissez un point de terminaison de service WCF qui utilise NetMessagingBinding pour recevoir les messages d'un abonnement, procédez comme suit :

    • Comme valeur de l'attribut address, vous devez spécifier l'URL du sujet auquel l'abonnement appartient. L'URL du sujet est fournie par la concaténation de l'URL de l'espace de noms du service et le nom du sujet.

    • Comme valeur de l'attribut listenUri, vous devez spécifier l'URL de l'abonnement. L'URL de l'abonnement est définie par la concaténation de l'URL du sujet, la chaîne /Subscriptions/ et l'espace de noms de l'abonnement.

    • Affectez la valeur Explicit à l'attribut listenUriMode. La valeur par défaut de listenUriMode est Explicit, par conséquent, ce paramètre est facultatif.

  • Les lignes [100-102] spécifient la version du CLR (Common Langage Runtime) prise en charge par l'application.

Examinons maintenant les étapes nécessaires pour créer un service de workflow WCF, qui reçoit une demande et renvoie une réponse via des files d'attente et rubriques Service Bus. Au moment de la création initiale du service de flux de travail WCF, il ne contenait qu'une activité Sequence avec une activité Receive suivie d'une activité SendReply, comme indiqué dans l'illustration suivante.

Bus de service-Files d'attente-Rubriques-WCF-Flux de travail-Service12

Dans un premier temps, j'ai cliqué sur la surface du flux de travail et j'ai affecté la chaîne CalculatorService comme valeur des propriétés ConfigurationName et Name de WorkflowService, comme le montre l'image ci-après. Notamment, la propriété ConfigurationName indique le nom de la configuration du service de flux de travail et sa valeur doit être égale à la valeur de l'attribut name de l'élément service dans le fichier de configuration.

Bus de service-Files d'attente-Rubriques-WCF-Flux de travail-Service13

Puis, j'ai sélectionné l'activité Sequential, j'ai cliqué sur le bouton Variables pour afficher l'éditeur correspondant, et j'ai créé les variables suivantes :

  • calculatorRequest : cette variable est de type CalculatorRequest et, comme le suggère son nom, contient le corps du message de demande. Sa valeur est définie par l'activité Receive utilisée pour récupérer le message de demande de requestqueue ou de l'abonnement ItalyMilan de requesttopic.

  • calculatorResponse : cette variable est de type CalculatorResponse et contient le corps du message de réponse renvoyé à l'application cliente. Sa valeur est définie par une activité personnalisée qui traite la demande et génère une réponse.

  • inboundBrokeredMessageProperty : cette variable contient le BrokeredMessageProperty du message de demande. Sa valeur est définie par une instance de BrokeredMessagePropertyActivity qui encapsule l'activité Receive et lit le BrokeredMessageProperty à partir des propriétés du message de demande WCF.

  • outboundBrokeredMessageProperty : cette variable contient le BrokeredMessageProperty du message de réponse. Sa valeur est définie par l'activité personnalisée qui traite la demande et génère une réponse. BrokeredMessagePropertyActivity est utilisé pour encapsuler l'activité Send et assigner la valeur de cette variable au BrokeredMessageProperty du message de réponse WCF.

Bus de service-Files d'attente-Rubriques-WCF-Flux de travail-Service14

J'ai ensuite ajouté une activité TryCatch au flux de travail et j'ai encapsulé l'activité Receive avec une instance de BrokeredMessagePropertyActivity, comme le montre l'image suivante.

Bus de service-Files d'attente-Rubriques-WCF-Flux de travail-Service15

Ensuite, j'ai cliqué sur BrokeredMessagePropertyActivity et j'ai affecté la variable inboundBrokeredMessageProperty à sa propriété BrokeredMessageProperty, comme le montre l'image ci-après.

Bus de service-Files d'attente-Rubriques-WCF-Flux de travail-Service16

Puis, j'ai sélectionné l'activité Receive et j'ai configuré ses propriétés pour recevoir des messages de demande de requestqueue et de l'abonnement ItalyMilan de requesttopic à l'aide, respectivement, des points de terminaison de service RequestQueueServiceEndpoint et RequestTopicServiceEndpoint définis dans le fichier de configuration.

Bus de service-Files d'attente-Rubriques-WCF-Flux de travail-Service17

Notamment, j'ai utilisé la propriété ServiceContractName de l'activité Receive pour spécifier l'espace de noms cible et le nom du contrat du point de terminaison de service, et j'ai utilisé la propriété Action pour spécifier l'en-tête de l'action du message de demande, tel que spécifié dans le contrat de service d'ICalculatorRequest.

J'ai ensuite ajouté une instance de CalculatorActivity au service de flux de travail WCF, sous BrokeredMessagePropertyActivity, et j'ai configuré ses propriétés comme le montre l'image suivante.

Bus de service-Files d'attente-Rubriques-WCF-Flux de travail-Service18

Ensuite, j'ai ajouté une activité If au flux de travail, sous CalculatorActivity, et j'ai configuré sa propriété Condition comme suit :

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

J'ai ensuite créé une instance de BrokeredMessagePropertyActivity dans les deux branches de l'activité If, et j'ai ajouté une activité Send à chacune, comme le montre l'image suivante.

Bus de service-Files d'attente-Rubriques-WCF-Flux de travail-Service19

De cette façon, si l'adresse de réponse spécifiée dans la propriété de ReplyTo du BrokeredMessageProperty entrant contient la chaîne « sujet », la réponse est envoyée à responsetopic, et sinon, la réponse est envoyée à responsequeue.

Dans les deux cas, une instance de BrokeredMessagePropertyActivity (identifiée par le nom complet Set BrokeredMessage) permet d'encapsuler l'activité Send et d'assigner l'objet BrokeredMessageProperty sortant à la collection de propriétés du message de réponse WCF. Pour effectuer cette opération, j'ai attribué la valeur de la variable outboundBrokeredMessageProperty à la propriété BrokeredMessageProperty des deux instances de BrokeredMessagePropertyActivity, comme le montre l'image ci-après.

Bus de service-Files d'attente-Rubriques-WCF-Flux de travail-Service20

Ensuite, j'ai sélectionné l'activité Send dans la branche Then et j'ai configuré sa propriété comme suit pour envoyer le message de réponse à responsetopic à l'aide du ResponseTopicClientEndpoint défini dans le fichier de configuration.

Bus de service-Files d'attente-Rubriques-WCF-Flux de travail-Service21

En particulier, j'ai utilisé la propriété ServiceContractName de l'activité Send pour spécifier l'espace de noms cible et le nom du contrat du point de terminaison du client, la propriété Action pour spécifier l'en-tête de l'action du message de réponse tel que spécifié dans le contrat de service d'ICalculatorResponse, et le EndpointConfigurationName pour indiquer le nom du point de terminaison du client défini dans le fichier de configuration.

De même, j'ai configuré l'activité Send dans la branche Else, comme indiqué dans l'illustration suivante.

Bus de service-Files d'attente-Rubriques-WCF-Flux de travail-Service22

L'image suivante présente la totalité du flux de travail :

Bus de service-Files d'attente-Rubriques-WCF-Flux de travail-Service23

En supposant que vous avez configuré correctement la solution, vous pouvez procéder comme suit pour la tester.

  • Pour envoyer un message de demande au service de flux de travail WCF via requestqueue, sélectionnez la case d'option File d'attente dans le groupe Méthodes de demande.

  • Pour envoyer un message de demande au service de flux de travail WCF via requesttopic, sélectionnez la case d'option Sujet dans le groupe Méthodes de demande.

  • Pour demander au service de flux de travail WCF d'envoyer un message de réponse à requestqueue, sélectionnez la case d'option File d'attente dans le groupe Méthodes de réponse.

  • Pour demander au service de flux de travail WCF d'envoyer un message de réponse à responsetopic, sélectionnez la case d'option Sujet dans le groupe Méthodes de réponse.

La figure ci-dessous montre la combinaison la plus intéressante :

  • Le client envoie un message de demande à requesttopic.

  • Le service de flux de travail WCF lit la demande de l'abonnement ItalyMilan pour requesttopic et envoie la réponse à responsetopic.

  • L'application cliente reçoit le message de réponse de l'abonnement ItalyMilan défini sur responsetopic.

L'illustration suivante montre les informations enregistrées par le service de flux de travail WCF sur la sortie standard de l'application console hôte :

Bus de service-Files d'attente-Rubriques-WCF-Flux de travail-Service24

L'illustration suivante montre les informations enregistrées par l'application cliente pendant l'appel :

Bus de service-Files d'attente-Rubriques-WCF-Flux de travail-Service25

En particulier, vous pouvez noter ce qui suit :

  1. Le message de demande a été envoyé à requesttopic.

  2. Le message de réponse a été reçu de responsetopic.

  3. Le CorrelationId du message de réponse est égal au MessageId du message de demande.

  4. La propriété Label du message de demande a été copiée par le service de flux de travail WCF dans la propriété Label du message de réponse.

  5. Toutes les propriétés définies par l'utilisateur ont été copiées par le service de flux de travail WCF du message de demande vers le message de réponse.

  6. Le service de flux de travail WCF a ajouté la propriété définie par l'utilisateur Source à la réponse.

  7. La propriété Area a été ajoutée par l'action de la règle définie sur l'abonnement ItalyMilan.

Dans cet article, nous avons découvert comment intégrer un service de workflow WCF à la messagerie répartie Service Bus, et atteindre une interopérabilité complète entre ces deux technologies, en utilisant simplement leurs fonctionnalités natives et une activité personnalisée pour gérer l'objet BrokeredMessageProperty. J'attends avec impatience vos commentaires. En attendant, vous pouvez télécharger le code de l'article dans MSDN Code Gallery.

Cela vous a-t-il été utile ?
(1500 caractères restants)
Merci pour vos suggestions.
Microsoft réalise une enquête en ligne pour recueillir votre opinion sur le site Web de MSDN. Si vous choisissez d’y participer, cette enquête en ligne vous sera présentée lorsque vous quitterez le site Web de MSDN.

Si vous souhaitez y participer,
Afficher:
© 2015 Microsoft