Exporter (0) Imprimer
Développer tout

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

Mis à jour: mars 2014

Auteur : Paolo Salvatori

Réviseurs : Ralph Squillace, Sidney Higa

Ce document présente le service de messagerie relayée Service Bus et vous aide à intégrer un service de flux de travail WCF aux files d'attente et aux sujets Service Bus. Il vous guidera dans la création d'un service de flux de travail WCF qui utilise les files d'attente et les sujets pour échanger des messages avec une application cliente. En complément de ce guide, je vous recommande fortement l'excellent l'article Utilisation de la messagerie du Service Bus de Azure par Roman Kiss qui propose une solution plus sophistiquée au même problème, et une série d'activités réutilisables dans un service de flux de travail WCF pour interagir avec les files d'attente et les sujets Service Bus.

Pour plus d'informations sur le Service Bus de Azure, reportez-vous aux ressources suivantes :

Les files d'attente fournissent des fonctionnalités de messagerie qui permettent à des applications hétérogènes exécutées sur un site ou dans le cloud d'échanger des messages de manière souple, sécurisée et fiable au sein d'un réseau et de délimitations d'approbation.

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

Les files d'attente sont hébergées dans Azure par une infrastructure de magasin répliquée et durable. La taille maximale d'une file d'attente est de 5 Go. La taille maximale d'un message est de 256 Ko, mais vous pouvez utiliser des sessions pour créer des séquences de messages associés de taille illimitée. Les files d'attente sont accessibles via les API suivantes :

Les entités de file d'attente fournissent les fonctionnalités suivantes :

  • Corrélation basée sur la session, ce qui signifie que vous pouvez générer des chemins d'accès de demande/réponse multiplexés. Dans un scénario de consommateurs concurrents où plusieurs processus de travail reçoivent des messages de la même file d'attente ou abonnement compatible avec la session, cela garantit que les messages qui partagent le même SessionId sont reçus par le même client.

  • Possibilité de spécifier l'heure à laquelle le message est ajouté à la file d'attente.

  • Modèles de remise fiables au moyen du mode de réception PeekLock (un message reste dans la file d'attente jusqu'à ce qu'il soit définitivement traité).

  • Prise en charge des transactions, pour garantir que les séries d'opérations de messagerie sont validées atomiquement. Cette fonctionnalité permet à une application d'envoyer plusieurs messages dans le contexte de la même transaction et, par conséquent, de valider ou interrompre l'opération entière comme un élément de travail unique.

  • Détection des doublons de messages entrants, pour que les clients puissent envoyer le même message plusieurs fois sans conséquences défavorables.

  • Fonctionnalité de lettres mortes pour les messages qui ne peuvent pas être traités ou qui expirent avant d'être reçus.

  • Ajournement des messages pour un traitement ultérieur. (Cette fonctionnalité est particulièrement pratique lorsque des messages sont reçus en dehors de la séquence attendue et doivent être mis de côté en toute sécurité pendant que le processus attend un message spécifique avant de poursuivre, ou lorsque des messages doivent être traités en fonction d'un jeu de propriétés qui définissent leur priorité pendant un pic de trafic.)

Les deux types les plus importants dans l'API .NET pour les messages relayés (les messages utilisés dans les files d'attente Service Bus, les sujets et leurs abonnements) sont représentés par la classe BrokeredMessage (qui expose des propriétés telles que MessageId, SessionID et CorrelationId, qui activent la détection automatique des doublons et les communications compatibles avec la session, entre autres choses) et la classe QueueDescription, qui permet de contrôler le comportement de la file d'attente créée. La classe QueueDescription contient les propriétés importantes suivantes :

  • La propriété DefaultMessageTimeToLive spécifie la valeur par défaut de la durée de vie du message.

  • La propriété DuplicateDetectionHistoryTimeWindow définit la durée de l'historique de la détection des doublons.

  • La propriété EnableDeadLetteringOnMessageExpiration vous permet d'activer\désactiver les lettres mortes à l'expiration du message.

  • La propriété LockDuration définit la durée du verrou utilisé par un consommateur en cas d'utilisation du mode de réception PeekLock.

  • La propriété MaxSizeInMegabytes définit la taille maximale de la file d'attente en mégaoctets.

  • La propriété RequiresDuplicateDetection active\désactive la détection des messages en double.

  • La propriété RequiresSession active\désactive les sessions.

  • MessageCount retourne le nombre de messages dans la file d'attente. (Un système intelligent peut l'utiliser pour déterminer si augmenter ou réduire le nombre de consommateurs concurrents qui reçoivent et traitent simultanément les messages de la file d'attente.)

noteRemarque
Étant donné que les métadonnées ne peuvent pas être modifiées après la création d'une entité de messagerie, la modification du comportement de détection des doublons nécessite la suppression et la recréation de la file d'attente. Le même principe s'applique à toutes les autres métadonnées.

Du point de vue architectural, les files d'attente sont un élément important des applications distribuées car elles vous permettent de lisser un trafic hautement variable dans un flux de travail prédictible, puis de répartir la charge sur un ensemble de processus de travail dont la taille peut être modifiée dynamiquement en fonction du volume des messages entrants. Dans un scénario de consommateurs concurrents, lorsqu'un éditeur écrit un message dans une file d'attente, plusieurs consommateurs entrent en concurrence pour le recevoir, mais un seul d'entre eux reçoit et traite le message en question. En d'autres termes, une file d'attente peut avoir un consommateur unique qui reçoit tous les messages, ou un ensemble de consommateurs concurrents qui extraient des messages sur une base « premier arrivé premier servi ». Pour cette raison, les files d'attente sont une excellente solution de messagerie pour répartir la charge de travail sur plusieurs ensembles de processus de travail concurrents.

Dans les architectures orientées services ou Service Bus, composées de plusieurs systèmes hétérogènes, les interactions entre les systèmes autonomes sont asynchrones et faiblement couplées. Dans ce contexte, les utilisateurs recourent souvent aux services SOAP ou REST pour assurer le faible couplage des composants, mais les entités de messagerie Service Bus, comme les files d'attente et les sujets (voir la section suivante) augmentent l'agilité, l'évolutivité et la souplesse de l'architecture globale et vous aident à réduire le faible couplage des systèmes individuels.

Pour plus d'informations sur les files d'attente, voir les articles suivants :

Les rubriques étendent les fonctionnalités de messagerie fournies par les files d'attente et y ajoutent des fonctions de publication-abonnement.

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

Une entité de sujet est une banque de messages séquentiels semblable à une file d'attente, mais prend en charge jusqu'à 2000 abonnements simultanés et durables qui transmettent des copies du message à un ensemble de processus de travail (ce nombre peut varier à l'avenir). Comme illustré dans la figure suivante, chaque abonnement peut définir une ou plusieurs entités de règle.

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

Chaque règle spécifie une expression de filtre utilisée pour filtrer les messages qui passent par l'abonnement, et une action de filtrage qui peut modifier les propriétés des messages. Notamment, la classe SqlFilter vous permet de définir une condition de type SQL92 sur les propriétés du message :

  • OrderTotal > 5000 OR ClientPriority > 2

  • ShipDestinationCountry = ‘USA’ AND ShipDestinationState = ‘WA’

Pour plus d'informations à ce sujet, consultez la documentation afférente à la propriété SqlExpression.

Inversement, la classe SqlRuleAction peut être utilisée pour ajouter, modifier ou supprimer des propriétés sur un objet BrokeredMessage à l'aide d'une syntaxe similaire à celle utilisée par la clause SET d'une instruction SQL UPDATE.

  • SET AuditRequired = 1

  • SET Priority = 'High', Severity = 1

noteRemarque
Chaque règle de correspondance qui définit explicitement une action génère une copie distincte du message publié, par conséquent, tout abonnement peut potentiellement générer des copies du même message, une pour chaque règle de correspondance.

Comme les files d'attente, les sujets prennent en charge un scénario de consommateurs concurrents. Dans ce contexte, un abonnement peut avoir un consommateur unique qui reçoit tous les messages, ou un ensemble de consommateurs concurrents qui extraient des messages sur une base « premier arrivé premier servi ». Les sujets sont une excellente solution de messagerie pour diffuser des messages à de nombreuses applications consommatrices ou répartir la charge de travail sur plusieurs ensembles de processus de travail concurrents.

Pour plus d'informations sur les sujets, consultez la documentation suivante :

La classe BrokeredMessage modèle les messages échangés par des applications qui communiquent avec les files d'attente et les sujets. La classe fournit 4 constructeurs publics différents :

La classe expose un jeu intéressant de méthodes qui permettent l'exécution d'un large éventail d'actions au niveau du message :

  • Lorsque vous utilisez le mode de réception PeekLock, la méthode Abandon permet de libérer le verrou sur un message verrouillé en lecture, tandis que la méthode Complete valide l'opération de réception d'un message et indique que le message doit être marqué comme traité et supprimé ou archivé.

  • La méthode Defer indique que le récepteur souhaite reporter le traitement du message. Comme indiqué précédemment, le report des messages est un moyen pratique de gérer les situations où les messages sont reçus hors de la séquence attendue et doivent être parqués sans risque pendant que les applications attendent un message spécifique pour continuer le traitement du flux de messages.

  • Les méthodes DeadLetter et DeadLetter(String, String) permettent à une application de déplacer explicitement un message dans la file d'attente des lettres mortes d'une file d'attente ou d'un abonnement. Notez que lorsque vous créez une entité de file d'attente avec l'API de gestion ou le portail de gestion Azure, vous pouvez le configurer pour déplacer automatiquement les messages arrivés à expiration dans la file d'attente de lettres mortes. De la même manière, vous pouvez configurer un abonnement pour déplacer les messages arrivés à expiration et les messages qui ne passent pas l'évaluation du filtre, dans sa file d'attente de lettres mortes.

La classe BrokeredMessage expose un large éventail de propriétés :

  • La propriété ContentType vous permet de spécifier le type de contenu.

  • MessageId est l'identificateur du message.

  • La propriété CorrelationId peut être utilisée pour implémenter un modèle d'échange de messages de demande-réponse où l'application cliente utilise la propriété MessageId d'un message de demande sortant et la propriété CorrelationId d'un message de réponse entrant pour mettre en corrélation les deux messages. (Nous verrons une implémentation de cette technique plus loin dans cet article.)

  • La propriété SessionId permet de définir ou d'obtenir l'identificateur de la session d'un message. Dans un scénario de consommateurs concurrents où plusieurs processus de travail reçoivent des messages de la même la file d'attente ou abonnement compatible avec la session, cela garantit que les messages qui partagent le même SessionId sont reçus par le même client. Dans ce contexte, lorsqu'une application cliente A envoie un flux de messages de requête à une application serveur B via une file d'attente ou un sujet compatible avec la session, et attend les messages de réponse corrélés sur une file d'attente ou un abonnement compatible avec la session et distinct, l'application cliente A peut affecter l'ID de la session de réception à la propriété ReplyToSessionId des messages sortants pour indiquer à l'application B la valeur à affecter à la propriété SessionId des messages de réponse.

  • La propriété ReplyTo obtient ou définit l'adresse de réponse de la file d'attente. Dans un scénario de requête-réponse asynchrone, lorsqu'une application cliente A envoie un message de demande à une application serveur B via une file d'attente ou un sujet Service Bus, et attend un message de réponse, par convention, l'application cliente A peut utiliser la propriété ReplyTo du message de demande pour indiquer à l'application serveur B l'adresse de la file d'attente ou du sujet où envoyer la réponse. (Nous verrons une application de cette technique plus loin dans cet article.) La propriété Label obtient ou définit l'étiquette spécifique de l'application à des fins de personnalisation

  • La propriété SequenceNumber retourne le nombre unique attribué par Service Bus à un message. Cette propriété permet de récupérer un message différé à partir d'une file d'attente ou d'un abonnement.

  • La propriété TimeToLive permet de définir ou de consulter la valeur de durée de vie du message. Service Bus n'applique pas une durée de vie maximale pour les messages en attente de traitement dans une file d'attente ou un abonnement. Toutefois, vous pouvez définir une durée de vie par défaut lorsque vous créez une file d'attente, un objet ou un abonnement, ou bien, vous pouvez définir explicitement le délai d'expiration au niveau d'un message à l'aide de la propriété TimeToLive.

  • DeliveryCount retourne le nombre de remises de message.

  • La collection Properties permet de définir les propriétés spécifiques à l'application du message. C'est probablement la fonctionnalité la plus importante d'une entité BrokeredMessage car les propriétés définies par l'utilisateur peuvent être utilisées pour :

    • Transporter la charge utile d'un message. Dans ce contexte, le corps du message peut être vide.

    • Définir les propriétés spécifiques à l'application qui peuvent être utilisées par un processus de travail pour déterminer comment traiter le message actuel.

    • Spécifier le filtre et les expressions d'action qui peuvent être utilisés pour définir les règles d'enrichissement du routage et des données au niveau de l'abonnement.

Si vous connaissez les propriétés de contexte dans un message BizTalk, il est utile de considérer que les propriétés définies par l'utilisateur contenues dans la collection Properties de BrokeredMessage sont identiques aux propriétés de contexte d'un message BizTalk. Un autre exemple de conteneur de propriétés utilisé pour transmettre les informations de contexte est WCF, qui fournit un ensemble spécial de liaisons compatibles avec le contexte, comme BasicHttpContextBinding, NetTcpContextBinding ou WSHttpContextBinding, permettant l'envoi de paramètres supplémentaires au service pour échanger le contexte à l'aide de HttpCookies ou de l'en-tête SOAP. En fait, la collection BrokeredMessage Properties peut être utilisée pour transporter une partie des informations, voire la totalité de la charge utile ; puis, à l'aide des sujets et des abonnements, ces propriétés peuvent être utilisées pour acheminer le message à la destination appropriée. Par conséquent, dans un scénario où un système tiers échange des messages avec une application BizTalk via Service Bus, il est essentiel de convertir les propriétés spécifiques à l'application transportées par l'objet BrokeredMessage en propriétés de contexte de messages BizTalk, et vice versa. Dans l'article et dans l'exemple de code, je vous indiquerai comment obtenir ce résultat.

noteRemarque
Comme indiqué dans l'article Quotas du Bus des services Azure AppFabric, la taille maximale de chaque propriété est 32 K. La taille cumulée de toutes les propriétés ne peut pas dépasser 64 K. Cela s'applique à l'en-tête complet de BrokeredMessage, qui contient des propriétés d'utilisateur ainsi que des propriétés système (telles que SequenceNumber, Label, MessageId, etc.). L'espace occupé par les propriétés est compris dans la taille générale du message, dont la taille maximale est de 256K. Si une application dépasse l'une des limites indiquées ci-dessus, une exception SerializationException est levée, vous devez donc vous attendre à gérer cette condition d'erreur.

La messagerie relayée Service Bus prend en charge le modèle de programmation WCF et fournit notamment une nouvelle liaison appelée NetMessagingBinding, qui peut être utilisée par les applications compatibles WCF pour envoyer et recevoir des messages via des files d'attente, des sujets et des abonnements, via le protocole SBMP (Service Bus Messaging Protocol). NetMessagingBinding est le nouveau nom de la liaison pour les files d'attente et les sujets, totalement intégrée à WCF. D'un point de vue fonctionnel, la liaison NetMessagingBinding est semblable à la liaison NetMsmqBinding, qui prend en charge la mise en file d'attente à l'aide de MSMQ (Message Queuing) comme transport et permet la prise en charge des applications faiblement couplées. Du côté service, la liaison NetMessagingBinding fournit une pompe de messages automatique qui retire les messages d'une file d'attente ou d'un abonnement et est intégrée au mécanisme ReceiveContext de WCF.

La nouvelle liaison prend en charge les interfaces standard IInputChannel, IOutputChannel, IInputSessionChannel. 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, et encodé. Pour définir les propriétés spécifiques de BrokeredMessage, vous devez créer un objet BrokeredMessageProperty, définir ses propriétés et l'ajouter à la collection Properties du Message WCF, comme indiqué dans le tableau suivant. Lorsque vous utilisez la liaison NetMessagingBinding pour écrire un message dans la file d'attente ou dans un sujet, la classe interne ServiceBusOutputChannel recherche l'objet BrokeredMessageProperty dans la collection Properties du message WCF et copie toutes ses propriétés dans l'objet BrokeredMessage qu'elle crée. Elle copie ensuite la charge utile du message WCF dans l'objet BrokeredMessage, puis publie le message résultant dans la file d'attente ou le sujet cible.

static void Main(string[] args)
{
    try
    {
        // Create the 
        var channelFactory = new ChannelFactory<IOrderService>("orderEndpoint");
        var clientChannel = channelFactory.CreateChannel();
        
        // Create a order object
        var order = new Order()
                        {
                            ItemId = "001",
                            Quantity = 10
                        };

        // Use the OperationContextScope to create a block within which to access the current OperationScope
        using (var scope = new OperationContextScope((IContextChannel)clientChannel))
        {
            // Create a new BrokeredMessageProperty object
            var property = new BrokeredMessageProperty();

            // Use the BrokeredMessageProperty object to set the BrokeredMessage properties
            property.Label = "OrderItem";
            property.MessageId = Guid.NewGuid().ToString();
            property.ReplyTo = "sb://acme.servicebus.windows.net/invoicequeue";

            // Use the BrokeredMessageProperty object to define application-specific properties
            property.Properties.Add("ShipCountry", "Italy");
            property.Properties.Add("ShipCity", "Milan");

            // Add BrokeredMessageProperty to the OutgoingMessageProperties bag provided 
            // by the current Operation Context 
            OperationContext.Current.OutgoingMessageProperties.Add(BrokeredMessageProperty.Name, property);
            clientChannel.SendOrder(order);
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

De même, lorsque vous utilisez un point de terminaison de service basé sur NetMessagingBinding pour recevoir les messages d'une file d'attente ou d'un sujet, une application peut récupérer l'objet BrokeredMessageProperty de la collection Properties du message WCF entrant, comme indiqué dans le tableau suivant. Notamment, à la réception du message, les classes internes ServiceBusInputChannel et ServiceBusInputSessionChannel (cette dernière est utilisée pour recevoir les messages des files d'attente et des abonnements de la session) créent un nouveau Message WCF et copient la charge utile du corps du BrokeredMessage entrant dans le corps du message WCF qui vient d'être créé. Ensuite, elles copient les propriétés du BrokeredMessage entrant dans une nouvelle instance de la classe BrokeredMessageProperty et, enfin, ajoutent ces dernières à la collection Properties du message WCF entrant.

[ServiceBehavior]
public class OrderService : IOrderService
{
    [OperationBehavior]
    public void ReceiveOrder(Order order)
    {
        // Get the BrokeredMessageProperty from the current OperationContext
        var incomingProperties = OperationContext.Current.IncomingMessageProperties;
        var property = incomingProperties[BrokeredMessageProperty.Name] as BrokeredMessageProperty;

        ...
    }
}

Étant donné que Service Bus ne prend pas en charge IOutputSessionChannel, toutes les applications qui envoient des messagesaux files d'attente de la session doivent utiliser un contrat de service dont la propriété SessionMode est différente de SessionMode.Required. Toutefois, le runtime WCF Service Bus prend en charge IInputSessionChannel, de sorte que pour recevoir les messages d'une file d'attente ou d'un abonnement de session au moyen de WCF et de NetMessagingBinding, une application doit implémenter un contrat de service compatible avec la session. L'extrait de code suivant présente un service WCF qui reçoit des messages d'une file d'attente ou d'abonnement de session.

// ServiceBus does not support IOutputSessionChannel.
// All senders sending messages to sessionful queue must use a contract which does not enforce SessionMode.Required.
// Sessionful messages are sent by setting the SessionId property of the BrokeredMessageProperty object.
[ServiceContract]
public interface IOrderService
{
    [OperationContract(IsOneWay = true)]
    [ReceiveContextEnabled(ManualControl = true)]
    void ReceiveOrder(Order order);
}

// ServiceBus supports both IInputChannel and IInputSessionChannel. 
// A sessionful service listening to a sessionful queue must have SessionMode.Required in its contract.
[ServiceContract(SessionMode = SessionMode.Required)]
public interface IOrderServiceSessionful : IOrderService
{
}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, ConcurrencyMode = ConcurrencyMode.Single)]
public class OrderService : IOrderServiceSessionful
{
    [OperationBehavior]
    public void ReceiveOrder(Order order)
    {
        // Get the BrokeredMessageProperty from the current OperationContext
        var incomingProperties = OperationContext.Current.IncomingMessageProperties;
        var property = incomingProperties[BrokeredMessageProperty.Name] as BrokeredMessageProperty;

        ...

        //Complete the Message
        ReceiveContext receiveContext;
        if (ReceiveContext.TryGet(incomingProperties, out receiveContext))
        {
            receiveContext.Complete(TimeSpan.FromSeconds(10.0d));
            ...
        }
        else
        {
            throw new InvalidOperationException("...");
        }
    }
}

Notez que la propriété ManualControl de l'attribut d'opération ReceiveContextEnabled a la valeur true. Cela exige que le service appelle explicitement la méthode ReceiveContext.Complete pour valider l'opération de réception. En fait, 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.

Notez également que la propriété ServiceBehavior.InstanceContextMode de la classe OrderService est définie sur InstanceContextMode.PerSession et que la propriété ConcurrencyMode est définie sur ConcurrencyMode.Single. De cette façon, le ServiceHost crée une nouvelle instance du service chaque fois qu'une session est disponible dans la file d'attente/l'abonnement référencés et utilise un thread unique pour recevoir les messages dans un ordre séquentiel. La durée de vie de l'instance du service est contrôlée par la propriété SessionIdleTimeout de NetMessagingBinding.

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.

    Files d'attente Service Bus - 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 par modifier un espace de noms existant de façon à inclure Service Bus. Vous pouvez accomplir cette tâche dans le portail de gestion Azure en cliquant, respectivement, 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 dans un fichier de liaison XML (BizTalk Server). En revanche, l'outil de l'explorateur 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 Service Bus permet d'exporter des entités dans un fichier XML et des les réimporter sur la même instance d'espace de noms Service Bus, ou sur une instance différente. 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. Cela permet l'authentification au moyen du service Access Control, et d'obtenir un jeton qui prouve à l'infrastructure Service Bus que l'application est autorisée à approvisionner 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 d'approvisionnement 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 = Console.ReadLine();

                // Read Service Bus Issuer Secret
                Console.ForegroundColor = ConsoleColor.Green;
                Console.Write("Service Bus Issuer Secret: ");
                Console.ForegroundColor = defaultColor;
                var issuerSecret = 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.CreateSharedSecretTokenProvider(issuerName, issuerSecret);
                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éé un projet dans ma solution, appelé ServiceContracts, et j'ai défini deux interfaces de contrat de service utilisées par l'application cliente pour, respectivement, envoyer et recevoir des messages des 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 flux de travail WCF sous-jacent de façon asynchrone via les entités de messagerie Service Bus, elle agit comme un client et une application de service en même temps. 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:                <sharedSecret issuerName="owner"
  50:                              issuerSecret="SHARED-SECRET" />
  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 différents 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 retourner la réponse en tant que paramètre de sortie au workflow.

  • OutboundBrokeredMessageProperty : cet OutArgument est utilisé par l'activité de code pour retourner 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 flux de travail WCF implémente les communications avec l'application cliente au moyen des files d'attente et des sujets 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 flux de travail WCF pour échanger des messages de demande et de réponse avec l'application cliente, via les files d'attente et les sujets 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:                <sharedSecret issuerName="owner" 
  49:                              issuerSecret="ISSUER_SECRET"/>
  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 flux de travail WCF qui reçoit une demande et renvoie une réponse via les files d'attente et les sujets 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 retourné à 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 flux de travail WCF avec la messagerie relayée Service Bus, et comment 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.

Afficher:
© 2014 Microsoft