Questa pagina è stata utile?
I suggerimenti relativi al contenuto di questa pagina sono importanti. Comunicaceli.
Altri suggerimenti?
1500 caratteri rimanenti
Esporta (0) Stampa
Espandi tutto

Come integrare un servizio del flusso di lavoro WCF con code e argomenti di Service Bus

Aggiornamento: luglio 2015

Autore: Paolo Salvatori

Revisori: Ralph Squillace, Sidney Higa

Questo documento fornisce materiale sussidiario sull'integrazione di un servizio di flusso di lavoro WCF con le code e gli argomenti del Bus di servizio di Microsoft Azure. L'argomento illustra nel dettaglio come compilare un servizio del flusso di lavoro WCF che usa code e argomenti per lo scambio di messaggi con un'applicazione client. Come documento complementare a questa guida, vedere l'articolo Uso della messaggistica del bus di servizio a cura di Roman Kiss, che fornisce una soluzione più sofisticata allo stesso problema e un set di attività riutilizzabili di cui è possibile avvalersi in un servizio del flusso di lavoro WCF per interagire con le code e gli argomenti del Bus di servizio.

Per altre informazioni su Bus di servizio di Microsoft Azure, vedere le risorse seguenti:

La figura seguente illustra l'architettura generale della demo:

Flusso-di-lavoro-WCF-per-gli-argomenti-relativi-alle-code-del-ServiceBus-Servizio 4

  1. In un'applicazione client viene usato un proxy WCF e NetMessagingBinding per inviare un messaggio di richiesta a requestqueue o requesttopic.

  2. Il servizio del flusso di lavoro WCF in esecuzione in un'applicazione console o in IIS riceve il messaggio di richiesta da requestqueue o dalla sottoscrizione ItalyMilan definita in requesttopic.

    Flusso-di-lavoro-WCF-per-gli-argomenti-relativi-alle-code-del-ServiceBus-Servizio 5
  3. Il servizio del flusso di lavoro WCF, illustrato nella figura precedente, esegue le azioni seguenti:

    • L'oggetto personalizzato BrokeredMessagePropertyActivity (identificato dal nome visualizzato Get BrokeredMessage) legge BrokeredMessageProperty dal messaggio in arrivo e assegna il relativo valore a una variabile del flusso di lavoro definita nell'attività Sequential più esterna.

    • L'attività Receive recupera il messaggio da requestqueue o dalla sottoscrizione ItalyMilan di requesttopic.

    • L'oggetto personalizzato CalculatorActivity riceve il messaggio in arrivo e BrokeredMessageProperty come argomenti di input, elabora il messaggio di richiesta e genera un messaggio di risposta e un oggetto BrokeredMessageProperty in uscita. Quando viene eseguita in un'applicazione console, l'attività tiene traccia delle proprietà dell'oggetto BrokeredMessageProperty in ingresso e in uscita nell'output standard.

    • L'attività If legge l'indirizzo contenuto nella proprietà ReplyTo dell'oggetto BrokeredMessageProperty in ingresso.

      • Se la stringa contiene la parola "topic", la risposta verrà inviata a responsetopic,

      • in caso contrario, verrà inviata a responsequeue.

    • In entrambi i casi, un'istanza di BrokeredMessagePropertyActivity (identificato dal nome visualizzato Set BrokeredMessage) consente di eseguire il wrapping dell'attività Send e di assegnare BrokeredMessageProperty in uscita alla raccolta di proprietà del messaggio di risposta WCF.

  4. Tramite il servizio del flusso di lavoro WCF il messaggio di risposta viene scritto in responsequeue o in responsetopic.

  5. Nell'applicazione client viene usato un servizio WCF con due endpoint distinti per recuperare il messaggio di risposta da responsequeue o da responsetopic. In un ambiente con più applicazioni client, deve essere usata una coda o una sottoscrizione distinta in ognuna di esse per ricevere messaggi di risposta da BizTalk. Altre informazioni sull'argomento vengono fornite più avanti in questo articolo.

Dopo la descrizione dell'architettura globale della demo è possibile a questo punto analizzare in dettaglio i componenti principali della soluzione.

La prima operazione da eseguire per configurare correttamente l'ambiente consiste nel creare le entità di messaggistica usate nella demo. La prima operazione da eseguire consiste nel fornire un nuovo spazio dei nomi del Bus di servizio o nel modificare uno spazio dei nomi esistente in modo da includere il Bus di servizio. È possibile completare questa attività dal portale di gestione di Azure facendo clic rispettivamente sul pulsante Nuovo o sul pulsante Modifica.

Il passaggio successivo consiste nel creare le entità coda, argomento e sottoscrizione richieste dalla demo. Come accennato nell'introduzione, è possibile scegliere tra varie opzioni per completare questa attività. Il modo più semplice consiste nell'usare il portale di gestione di Azure tramite i pulsanti disponibili nel comando Gestisci entità.

È possibile usare la visualizzazione albero di navigazione illustrata nel punto 2 per selezionare un'entità esistente e visualizzarne le proprietà nella barra verticale evidenziata nel punto 3. Per rimuovere l'entità selezionata, premere Elimina sulla barra dei comandi Gestisci entità.

noteNota
L'uso del portale di gestione di Azure è un modo pratico e semplice per gestire le entità di messaggistica in uno spazio dei nomi del Bus di servizio specificato. Tuttavia, almeno al momento, il set di operazioni che uno sviluppatore o un amministratore di sistema può eseguire usando la relativa interfaccia utente è piuttosto limitato. Ad esempio, il portale di gestione di Azure consente attualmente a un utente di creare code, argomenti e sottoscrizioni e definire le relative proprietà, ma non di creare o visualizzare regole per una sottoscrizione esistente. Al momento è possibile completare questa attività solo usando l'API di messaggistica .NET. In particolare, per l'aggiunta di una nuova regola a una sottoscrizione esistente è possibile usare i metodi AddRule(String, Filter) o AddRule(RuleDescription) esposti dalla classe SubscriptionClient, mentre per enumerare le regole di una sottoscrizione esistente, è possibile usare il metodo GetRules della classe NamespaceManager. Il portale di gestione di Azure attualmente non consente inoltre di eseguire le operazioni seguenti:

  1. Visualizzare correttamente le entità in modo gerarchico. Nel portale di gestione di Azure vengono presentate attualmente code, argomenti e sottoscrizioni con una visualizzazione albero semplice. È tuttavia possibile organizzare entità di messaggistica in una struttura gerarchica specificandone semplicemente il nome come percorso assoluto composto da più segmenti di percorso, ad esempio crm/prod/code/codaordini.

  2. Esportare le entità di messaggistica contenute in uno spazio dei nomi del Bus di servizio specificato in un file di binding XML (come in BizTalk Server). Lo strumento Bus di servizio Explorer consente invece di selezionare ed esportare

    1. Singole entità.

    2. Entità per tipo (code o argomenti).

    3. Entità contenute in un determinato percorso, ad esempio crm/prod/code.

    4. Tutte le entità in uno spazio dei nomi specificato.

  3. Importare code, argomenti e sottoscrizioni da un file XML in uno spazio dei nomi esistente. In Bus di servizio Explorer è supportata la capacità di esportare entità in un file XML e di reimportarle nello stesso o in un altro spazio dei nomi del Bus di servizio. Questa funzionalità è utile per eseguire il backup e il ripristino di uno spazio dei nomi o per trasferire semplicemente code e argomenti da uno spazio dei nomi di test a uno di produzione.

Service Bus Explorer consente a un utente di creare, eliminare e testare code, argomenti, sottoscrizioni e regole e rappresenta il complemento perfetto per il portale di gestione di Azure ufficiale.

Per praticità, è stata creata un'applicazione console denominata Provisioning che usa la funzionalità fornita dalla classe NamespaceManager per creare le code, gli argomenti e le sottoscrizioni necessari per la soluzione. All'avvio, nell'applicazione console vengono richieste le credenziali dello spazio dei nomi del servizio. Tali credenziali vengono usate per l'autenticazione con il servizio Controllo di accesso e per acquisire un token di accesso che indica all'infrastruttura del Bus di servizio che l'applicazione è autorizzata alla fornitura di nuove entità di messaggistica. Nell'applicazione viene quindi richiesto il valore da assegnare alle proprietà delle entità da creare, ad esempio EnabledBatchedOperations e EnableDeadLetteringOnMessageExpiration per le code. Nell'applicazione Provisioning vengono create le seguenti entità nello spazio dei nomi del Bus di servizio:

  1. Una coda denominata requestqueue usata dall'applicazione client per inviare messaggi di richiesta a BizTalk Server.

  2. Una coda denominata responsequeue usata da BizTalk Server per inviare messaggi di risposta all'applicazione client.

  3. Un argomento denominato requesttopic usato dall'applicazione client per inviare messaggi di richiesta a BizTalk Server.

  4. Un argomento denominato responsetopic usato da BizTalk Server per inviare messaggi di risposta all'applicazione client.

  5. Una sottoscrizione denominata ItalyMilan per requesttopic, usata da BizTalk Server per ricevere messaggi di richiesta da requesttopic. La sottoscrizione in questione dispone di una sola regola definita come segue:

    1. Filter: Country='Italy' e City='Milan'

    2. Action: Set Area='Western Europe'

  6. Una sottoscrizione denominata ItalyMilan per responsetopic, usata dall'applicazione client per ricevere messaggi di risposta da responsetopic. La sottoscrizione in questione dispone di una sola regola definita come segue:

    1. Filter: Country='Italy' e City='Milan'

    2. Action: Set Area='Western Europe'

Nella figura seguente è riportato l'output dell'applicazione console Provisioning.

Flusso-di-lavoro-WCF-per-gli-argomenti-relativi-alle-code-del-ServiceBus-Servizio 8

La tabella seguente contiene il codice dell'applicazione Provisioning:

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

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

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

            try
            {
                // Set window size
                Console.WindowWidth = 100;
                Console.BufferWidth = 100;
                Console.WindowHeight = 48;
                Console.BufferHeight = 48;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

noteNota
Non sono state verificate tutte le possibili combinazioni di proprietà per code e argomenti, pertanto è possibile che la demo non funzioni come previsto con tutte le configurazioni.

È stata avviata la definizione dei contratti dati per i messaggi di richiesta e risposta. I contratti dati offrono un meccanismo per eseguire il mapping dei tipi CLR .NET definiti nel codice e gli elementi XML Schema (XSD) definiti dall'organizzazione W3C (www.w3c.org). I contratti dati vengono pubblicati nei metadati del servizio, consentendo ai client di convertire la rappresentazione neutrale e indipendente dalla tecnologia dei tipi di dati nelle rispettive rappresentazioni native. Per altre informazioni su dati e contratti, è possibile leggere l'articolo seguente:

Nella soluzione è stato creato un progetto denominato DataContracts per definire le classi che rappresentano rispettivamente i messaggi di richiesta e di risposta. Per praticità, il relativo codice è riportato di seguito.

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
}

Sono stati quindi definiti i Contratti di servizio usati dall'applicazione client per lo scambio di messaggi con il Bus di servizio. A questo scopo è stato creato un nuovo progetto nella soluzione, denominato ServiceContracts, e sono state definite due interfacce del contratto di servizio usate dall'applicazione client rispettivamente per l'invio e la ricezione di messaggi dalle entità di messaggistica del Bus di servizio. In effetti sono state creare due diverse versioni del contratto di servizio usato per ricevere i messaggi di risposta:

  • L'interfaccia ICalculatorResponse viene usata per ricevere i messaggi di risposta da una coda o una sottoscrizione non con sessione.

  • L'interfaccia ICalculatorResponseSessionful eredita dal contratto di servizio ICalculatorResponse ed è contrassegnata con l'attributo [ServiceContract(SessionMode = SessionMode.Required)]. Questo contratto di servizio viene usato per ricevere i messaggi di risposta da una coda o una sottoscrizione non con sessione.

Si noti che i metodi definiti da tutti i contratti sono unidirezionali. Questo è un requisito obbligatorio.

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

A questo punto è possibile esaminare il codice dell'applicazione client.

Poiché nell'applicazione Windows Form vengono scambiati messaggi con il servizio del flusso di lavoro WCF sottostante in modalità asincrona tramite le entità di messaggistica del Bus di servizio, viene usata contemporaneamente come applicazione client e come applicazione di servizio. In Windows Form WCF vengono usati WCF e la classe NetMessagingBinding per eseguire le azioni seguenti:

  1. Inviare messaggi di richiesta a requestqueue.

  2. Inviare messaggi di richiesta a requesttopic.

  3. Ricevere messaggi di risposta da responsequeue.

  4. Ricevere messaggi di risposta dalla sottoscrizione ItalyMilan di responsetopic.

Si esaminerà quindi il file di configurazione dell'applicazione client che svolge un ruolo centrale nella definizione degli endpoint WCF client e servizio usati per comunicare con il Bus di servizio.

App.Config dell'applicazione client


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

Di seguito è riportata una breve descrizione degli elementi e delle sezioni principali del file di configurazione:

  • Righe [3-32]: definiscono un listener di traccia personalizzato chiamato LogTraceListener usato da ResponseHandlerService per scrivere il messaggio di risposta nel controllo del log dell'applicazione Windows Form.

  • Righe [34-36]: nella sezione di avvio sono specificate le versioni di Common Language Runtime supportate dall'applicazione.

  • Righe [46-53]: includono la definizione di securityBehavior usata dall'endpoint client e servizio per l'autenticazione con il Servizio di controllo di accesso. In particolare, la classe TransportClientEndpointBehavior viene usata per definire le credenziali Shared Secret. Per altre informazioni su come recuperare le credenziali dal portale di gestione di Azure, vedere il riquadro sottostante.

  • Righe [57-67]: contengono la configurazione di NetMessagingBinding usata dagli endpoint client e servizio per lo scambio di messaggi con il Bus di servizio.

  • Righe [71-76]: contengono la definizione di requestQueueClientEndpoint usata dall'applicazione per inviare messaggi di richiesta a requestqueue. L'attributo address dell'endpoint client viene fornito dalla concatenazione dell'URL dello spazio dei nomi del servizio e del nome della coda.

  • Righe [78-83]: contengono la definizione di requestTopicClientEndpoint usata dall'applicazione per inviare messaggi di richiesta a requesttopic. L'attributo address dell'endpoint client viene fornito dalla concatenazione dell'URL dello spazio dei nomi del servizio e del nome dell'argomento.

  • Righe [87-92]: contengono la definizione di responseQueueServiceEndpoint usata dall'applicazione per ricevere messaggi di risposta da responsequeue. L'attributo address dell'endpoint servizio viene fornito dalla concatenazione dell'URL dello spazio dei nomi del servizio e del nome della coda.

  • Righe [93-99]: contengono la definizione di responseSubscriptionServiceEndpoint usata dall'applicazione per ricevere messaggi di risposta dalla sottoscrizione ItalyMilan per responsetopic. Quando si definisce un endpoint di servizio WCF in cui viene usato NetMessagingBinding per ricevere messaggi da una sottoscrizione, è necessario procedere come indicato di seguito (per altre informazioni su questo aspetto, vedere il riquadro sottostante):

    • Come valore dell'attributo address è necessario specificare l'URL dell'argomento a cui appartiene la sottoscrizione. L'URL dell'argomento viene fornito dalla concatenazione dell'URL dello spazio dei nomi del servizio e del nome dell'argomento.

    • Come valore dell'attributo listenUri è necessario specificare l'URL della sottoscrizione. L'URL della sottoscrizione viene definito dalla concatenazione dell'URL dell'argomento, della stringa /Subscriptions/ e del nome della sottoscrizione.

    • Assegnare il valore Explicit all'attributo listenUriMode. Il valore predefinito per listenUriMode è Explicit, quindi questa impostazione è facoltativa.

noteNota
Quando si configura un endpoint servizio WCF per utilizzare i messaggi da una coda o una sottoscrizione con sessione, il contratto di servizio deve supportare le sessioni. Pertanto in questo esempio, quando si configura l'endpoint responseSubscriptionServiceEndpoint o responseQueueServiceEndpoint per ricevere rispettivamente da una coda e da una sottoscrizione con sessione, è necessario sostituire il contratto di servizio ICalculatorResponse con l'interfaccia con sessione ICalculatorResponseSessionful. Per altre informazioni, vedere la sezione Contratti di servizio più avanti in questo articolo.

noteNota
Nel Bus di servizio sono supportati tre tipi diversi di schemi di credenziali: SAML, Shared Secret e Simple Web Token, ma in questa versione di Service Bus Explorer sono supportate solo le credenziali Shared Secret. È tuttavia possibile estendere facilmente il codice per supportare altri schemi di credenziali. È possibile recuperare il segreto dell'autorità emittente nel portale di gestione di Azure facendo clic sul pulsante Visualizza dopo aver selezionato un determinato spazio dei nomi nella sezione Bus di servizio.

Verrà visualizzata la finestra di dialogo modale in cui è possibile recuperare la chiave facendo clic su Copia negli Appunti evidenziato in rosso.

noteNota
Per convenzione, il nome Autorità di certificazione predefinita è sempre proprietario.

noteNota
Quando si definisce un endpoint di servizio WCF in cui viene usato NetMessagingBinding per ricevere messaggi da una sottoscrizione, se si assegna erroneamente l'URL della sottoscrizione all'attributo address dell'endpoint di servizio (come riportato nella configurazione seguente), in fase di esecuzione si verificherà un evento FaultException simile al seguente:

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

Configurazione non corretta


<?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'errore è dovuto al fatto che l'intestazione To WS-Addressing del messaggio contiene l'indirizzo dell'argomento e non l'indirizzo della sottoscrizione:


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

Per configurare l'endpoint servizio per la ricezione di messaggi da una sottoscrizione, è necessario procedere come segue:

  • Come valore dell'attributo address è necessario specificare l'URL dell'argomento a cui appartiene la sottoscrizione. L'URL dell'argomento viene fornito dalla concatenazione dell'URL dello spazio dei nomi del servizio e del nome dell'argomento.

  • Come valore dell'attributo listenUri è necessario specificare l'URL della sottoscrizione. L'URL della sottoscrizione viene definito dalla concatenazione dell'URL dell'argomento, della stringa /Subscriptions/ e del nome della sottoscrizione.

  • Assegnare il valore Explicit all'attributo listenUriMode. Il valore predefinito per listenUriMode è Explicit, quindi questa impostazione è facoltativa.

Vedere la pagina seguente nel sito MSDN per una descrizione degli attributi address, listenUri e listenUriMode.

Configurazione corretta


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

Per eseguire la stessa attività tramite l'API, è necessario impostare correttamente il valore delle proprietà Address, ListenUri e ListenUriMode dell'istanza ServiceEndpoint come indicato in questa nota.

La tabella seguente illustra il codice usato dall'applicazione client per avviare ResponseHandlerService usato per leggere i messaggi di risposta da responsequeue e dalla sottoscrizione ItalyMilan di responsetopic. Nella sezione successiva verrà esaminato il codice del servizio.


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

Nella figura seguente viene illustrata l'interfaccia utente dell'applicazione client:

Flusso-di-lavoro-WCF-per-gli-argomenti-relativi-alle-code-del-ServiceBus-Servizio 11

I pulsanti di opzione contenuti nel gruppo Metodi di richiesta consentono di scegliere se inviare il messaggio di richiesta a requestqueue oppure a requesttopic, mentre i pulsanti di opzione contenuti nel gruppo Metodo di risposta consentono di scegliere se ricevere la risposta da responsequeue o dalla sottoscrizione ItalyMilan di responsetopic. Per comunicare la selezione all'applicazione BizTalk sottostante, nell'applicazione viene usato un oggetto BrokeredMessageProperty per assegnare il valore del campo privato responseQueueUri o responseTopicUri alla proprietà ReplyTo. La tabella seguente include il codice del metodo usato dall'applicazione client per inviare il messaggio al Bus di servizio. Per praticità, sono stati aggiunti commenti al codice per facilitarne la comprensione.


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

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

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

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

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

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

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

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

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

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

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

La tabella seguente contiene il codice del servizio WCF usato dall'applicazione client per recuperare e registrare i messaggi di risposta da responsequeue e dalla sottoscrizione ItalyMilan di responsetopic. Per ottenere questo risultato, nel servizio vengono esposti due endpoint diversi, ognuno dei quali usa NetMessagingBinding e riceve i messaggi da una delle due code. Ogni sottoscrizione può infatti essere considerata un coda virtuale che ottiene le copie dei messaggi pubblicati nell'argomento a cui appartengono. La tabella seguente illustra il codice della classe ResponseHandlerService. Come si noterà, il servizio recupera BrokeredMessageProperty dalla raccolta Properties dell'oggetto message WCF in ingresso e usa l'oggetto per accedere alle proprietà del messaggio di risposta. Poiché nel contratto di servizio ICalculatorResponse il metodo ReceiveResponse è decorato con [ReceiveContextEnabled (ManualControl = true)], l'acknowledgement di ricezione deve essere segnalato in modo esplicito tramite il metodo del servizio. Ciò richiede che tramite il servizio venga richiamato in modo esplicito il metodo ReceiveContext.Complete per eseguire il commit dell'operazione di ricezione. In realtà, come affermato all'inizio di questo articolo, quando la proprietà ManualControl è impostata su true, il messaggio ricevuto dal canale viene recapitato all'operazione di servizio con un blocco relativo al messaggio. È compito dell'implementazione del servizio chiamare Complete(TimeSpan) o Abandon(TimeSpan) per segnalare il completamento della ricezione del messaggio. In caso non venga chiamato uno di questi metodi, il blocco del messaggio verrà mantenuto fino alla scadenza dell'intervallo di timeout del blocco stesso. Se il blocco viene rilasciato (tramite la chiamata a Abandon(TimeSpan) o per timeout del blocco), il messaggio viene inviato di nuovo dal canale al servizio. La chiamata a Complete(TimeSpan) contrassegna il messaggio come ricevuto correttamente.

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.Count > 0)
                 {
                     fr (int i = 0; i < calculatrRespnse.Results.Count; 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
 }

I servizi del flusso di lavoro WCF forniscono un ambiente produttivo per creare operazioni o servizi durevoli e con esecuzione prolungata. I servizi del flusso di lavoro vengono implementati tramite attività WCF in grado di usare WCF per l'invio e la ricezione di dati. Una spiegazione dettagliata sulla compilazione di un servizio del flusso di lavoro WCF non rientra dall'ambito del presente articolo. Per altre informazioni sui servizi del flusso di lavoro WCF, vedere gli articoli seguenti:

Attività di messaggistica introdotte in WF 4.0 che consentono agli sviluppatori di esporre o utilizzare i servizi WCF in modo semplice e flessibile. In particolare, le attività di messaggistica consentono ai flussi di lavoro di inviare dati ad altri sistemi (Send, SendReply) e di ricevere dati da altri sistemi (Receive, ReceiveReply) usando WCF. Tali attività nascondono tuttavia un'elevata quantità di plumbing WCF. In particolare le Attività di messaggistica non forniscono l'accesso all'attuale OperationContext che è possibile usare per eseguire la seguente operazione:

  • Sul lato invio, è possibile usare OperationContext per includere intestazioni del messaggio aggiuntive nella busta SOAP o per aggiungere proprietà del messaggio al messaggio in uscita.

  • Sul lato ricezione, è possibile usare OperationContext per recuperare le proprietà del messaggio e le informazioni di sicurezza dal messaggio in arrivo.

Come descritto all'inizio dell'articolo, quando in un'applicazione vengono usati WCF e NetMessagingBinding per inviare un messaggio a una coda o un argomento, il messaggio viene inserito in una busta SOAP e codificato. Per impostare le proprietà specifiche di BrokeredMessage è necessario creare un oggetto BrokeredMessageProperty, impostare le proprietà su tale oggetto e aggiungerlo alla raccolta Properties dell'oggetto Message WCF. Di conseguenza, per recuperare BrokeredMessageProperty da un messaggio in arrivo o per aggiungere BrokeredMessageProperty alla raccolta Properties di un oggetto Message WCF in uscita, è necessario estendere la funzionalità integrata fornita dalle Attività di messaggistica. Fortunatamente, WF 4.0 consente di estendere il comportamento in fase di esecuzione delle attività di messaggistica tramite IReceiveMessageCallback e ISendMessageCallback. In particolare:

  • L'interfaccia IReceiveMessageCallback implementa una funzionalità di callback da eseguire quando un messaggio di servizio viene ricevuto dall'attività Receive.

  • L'interfaccia ISendMessageCallback.interface implementa una funzionalità di callback che viene chiamata prima dell'invio di un messaggio dall'attività Send.

Nella demo sono stati usati questi punti di estendibilità per creare un oggetto NativeActivity personalizzato denominato BrokeredMessagePropertyActivity che consente di:

  • Ottenere BrokeredMessageProperty dalle proprietà di un messaggio WCF in ingresso.

  • Impostare BrokeredMessageProperty per un messaggio WCF in uscita.

Nell'articolo di Roman Kiss è stato seguito lo stesso approccio ed è stata creata un'attività più sofisticata che espone una proprietà per ogni proprietà esposta dalla classe BrokeredMessageProperty. È consigliabile leggere tale articolo in quanto viene sostanzialmente illustrato un metodo alternativo per implementare la stessa tecnica descritta nel presente articolo.

Per praticità, nella tabella seguente è incluso il codice di BrokeredMessagePropertyActivity:

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
}

È possibile usare BrokeredMessagePropertyActivity per eseguire il wrapping dell'attività Receive o Send. In questo modo viene fornito automaticamente l'accesso a BrokeredMessageProperty che è possibile usare per leggere e scrivere proprietà esplicite e definite dall'utente di una classe BrokeredMessage

Per elaborare la richiesta in arrivo e generare una risposta, è stata creata un'attività codice personalizzata denominata CalculatorActivity, il cui codice è illustrato nella tabella seguente.

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

Questa attività espone rispettivamente due argomenti di input e due di output:

  • CalculatorRequest: questa classe InArgument di tipo CalculatorRequest consente al flusso di lavoro di passare la richiesta all'attività.

  • InboundBrokeredMessageProperty: questa classe InArgument di tipo BrokeredMessageProperty consente di passare BrokeredMessageProperty come parametro di input estratto dal messaggio di richiesta WCF.

  • CalculatorResponse: questa classe OutArgument di tipo CalculatorResponse viene usata dall'attività codice per restituire la risposta come parametro di output al flusso di lavoro.

  • OutboundBrokeredMessageProperty: questa classe OutArgument viene usata dall'attività codice per restituire la classe BrokeredMessageProperty in uscita al flusso di lavoro che userà un'istanza di BrokeredMessagePropertyActivity per assegnare il valore del parametro di output a BrokeredMessageProperty del messaggio WCF di risposta.

In breve, l'oggetto personalizzato CalculatorActivity riceve il messaggio in arrivo e BrokeredMessageProperty come argomenti di input, elabora il messaggio di richiesta e genera un messaggio di risposta e un oggetto BrokeredMessageProperty in uscita. Quando viene eseguita in un'applicazione console, l'attività tiene traccia delle proprietà dell'oggetto BrokeredMessageProperty in ingresso e in uscita nell'output standard.

In questa sezione si esaminerà in dettaglio la modalità di implementazione delle comunicazioni con l'applicazione client nel servizio del flusso di lavoro WCF usando code e argomenti del Bus di servizio. Nella demo il servizio del flusso di lavoro WCF è ospitato in un'applicazione console, ma è possibile modificare facilmente la soluzione per eseguire il servizio del flusso di lavoro WCF in un'applicazione ospitata in IIS in locale o in un ruolo di Azure nel cloud. La tabella seguente contiene il codice usato dall'applicazione console per inizializzare e aprire un oggetto WorkflowServiceHost. In particolare, l'endpoint HTTP locale specificato nel costruttore di oggetti può essere usato per il recupero di WSDL esposto dal servizio del flusso di lavoro WCF.

Classe Program


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

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

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

La tabella seguente contiene il file di configurazione dell'applicazione console che fornisce un ruolo chiave nella definizione degli endpoint client e servizio WCF, usati dal servizio del flusso di lavoro WCF, per scambiare messaggi di richiesta e risposta con l'applicazione client tramite code e argomenti del Bus di servizio.

App.Config dell'applicazione console


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

Di seguito è riportata una breve descrizione degli elementi e delle sezioni principali del file di configurazione. Si noti che la definizione di endpoint client e servizio è duplice in relazione alla configurazione degli endpoint client e servizio usati dall'applicazione client.

  • Righe [3-33]: abilitano l'analisi e consentono di configurare le origini di traccia per generare tracce e impostare il livello di traccia. In particolare, la sezione di diagnostica è configurata per tenere traccia dei messaggi a livello di trasporto e di servizio in un file di log che è possibile esaminare tramite lo Strumento Visualizzatore di tracce dei servizi (SvcTraceViewer.exe)

  • Righe [36-43]: specificano il comportamento predefinito del servizio usato dal servizio del flusso di lavoro WCF.

  • Righe [44-53]: includono la definizione di securityBehavior usata dall'endpoint client e servizio per l'autenticazione con il Servizio di controllo di accesso. In particolare, la classe TransportClientEndpointBehavior viene usata per definire le credenziali Shared Secret.

  • Righe [56-66]: contengono la configurazione di NetMessagingBinding usata dagli endpoint client e servizio per lo scambio di messaggi con il Bus di servizio.

  • Righe [69-74]: contengono la definizione di ResponseQueueClientEndpoint usata dal servizio del flusso di lavoro WCF per inviare messaggi di risposta a responsequeue. L'attributo address dell'endpoint client viene fornito dalla concatenazione dell'URL dello spazio dei nomi del servizio e del nome della coda.

  • Righe [78-83]: contengono la definizione di ResponseTopicClientEndpoint usata dal servizio del flusso di lavoro WCF per inviare messaggi di risposta a responsetopic. L'attributo address dell'endpoint client viene fornito dalla concatenazione dell'URL dello spazio dei nomi del servizio e del nome dell'argomento.

  • Righe [83-89]: contengono la definizione di RequestQueueServiceEndpoint usata dal servizio del flusso di lavoro WCF per ricevere messaggi di richiesta da requestqueue. L'attributo address dell'endpoint servizio viene fornito dalla concatenazione dell'URL dello spazio dei nomi del servizio e del nome della coda.

  • Righe [90-96]: contengono la definizione di RequestTopicServiceEndpoint usata dall'applicazione per ricevere messaggi di richiesta dalla sottoscrizione ItalyMilan per requesttopic. Quando si definisce un endpoint di servizio WCF in cui viene usata la classe NetMessagingBinding per ricevere messaggi da una sottoscrizione, è necessario procedere come segue:

    • Come valore dell'attributo address è necessario specificare l'URL dell'argomento a cui appartiene la sottoscrizione. L'URL dell'argomento viene fornito dalla concatenazione dell'URL dello spazio dei nomi del servizio e del nome dell'argomento.

    • Come valore dell'attributo listenUri è necessario specificare l'URL della sottoscrizione. L'URL della sottoscrizione viene definito dalla concatenazione dell'URL dell'argomento, della stringa /Subscriptions/ e del nome della sottoscrizione.

    • Assegnare il valore Explicit all'attributo listenUriMode. Il valore predefinito per listenUriMode è Explicit, quindi questa impostazione è facoltativa.

  • Righe [100-102]: specificano la versione di Common Language Runtime supportata dall'applicazione.

A questo punto si esamineranno i passaggi necessari per creare un servizio del flusso di lavoro WCF che riceve una richiesta e invia una risposta tramite code e argomenti del Bus di servizio. La prima volta che è stato creato questo servizio del flusso di lavoro WCF, conteneva solo un'attività Sequence con un'attività Receive seguita da un'attività SendReply, come mostrato nella figura seguente.

Flusso-di-lavoro-WCF-per-gli-argomenti-relativi-alle-code-del-ServiceBus-Servizio 12

Come primo passaggio, si è fatto clic sulla superficie del flusso di lavoro ed è stata assegnata la stringa CalculatorService come valore a entrambe le proprietà ConfigurationName e Name di WorkflowService, come illustrato nell'immagine seguente. In particolare, la proprietà ConfigurationName indica il nome della configurazione del servizio del flusso di lavoro e il relativo valore deve essere uguale al valore dell'attributo name dell'elemento service nel file di configurazione.

Flusso-di-lavoro-WCF-per-gli-argomenti-relativi-alle-code-del-ServiceBus-Servizio 13

È stata quindi selezionata l'attività Sequential, si è fatto clic sul pulsante relativo allevariabili per visualizzare l'editor corrispondente e sono state create le variabili seguenti:

  • calculatorRequest: questa variabile è di tipo CalculatorRequest e, come suggerisce il nome, contiene il corpo del messaggio di richiesta. Il relativo valore è impostato dall'attività Receive usata per ricevere il messaggio di richiesta da requestqueue o dalla sottoscrizione ItalyMilan di requesttopic.

  • calculatorResponse: questa variabile è di tipo CalculatorResponse e contiene il corpo del messaggio di risposta restituito all'applicazione client. Il relativo valore viene impostato da un'attività personalizzata che elabora la richiesta e produce una risposta.

  • inboundBrokeredMessageProperty: questa variabile contiene BrokeredMessageProperty del messaggio di richiesta. Il relativo valore viene impostato da un'istanza di BrokeredMessagePropertyActivity che esegue il wrapping dell'attività Receive e legge l'oggetto BrokeredMessageProperty dalle proprietà del messaggio di richiesta WCF.

  • outboundBrokeredMessageProperty: questa variabile contiene BrokeredMessageProperty del messaggio di richiesta. Il relativo valore viene impostato dall'attività personalizzata che elabora la richiesta e produce una risposta. Un oggetto BrokeredMessagePropertyActivity viene usato per eseguire il wrapping dell'attività Send e viene assegnato il valore di questa variabile all'oggetto BrokeredMessageProperty del messaggio di risposta WCF.

Flusso-di-lavoro-WCF-per-gli-argomenti-relativi-alle-code-del-ServiceBus-Servizio 14

È stata quindi aggiunta un'attività TryCatch al flusso di lavoro e è stato eseguito il wrapping dell'attività Receive con un'istanza di BrokeredMessagePropertyActivity, come illustrato nella figura seguente.

Flusso-di-lavoro-WCF-per-gli-argomenti-relativi-alle-code-del-ServiceBus-Servizio 15

È stato quindi fatto clic su BrokeredMessagePropertyActivity ed è stata assegnata la variabile inboundBrokeredMessageProperty alla relativa proprietà BrokeredMessageProperty, come illustrato nella figura seguente.

Flusso-di-lavoro-WCF-per-gli-argomenti-relativi-alle-code-del-ServiceBus-Servizio 16

È stata quindi selezionata l'attività Receive e sono state configurate le relative proprietà per ricevere messaggi di richiesta da requestqueue e dalla sottoscrizione ItalyMilan di requesttopic usando rispettivamente gli endpoint di servizio RequestQueueServiceEndpoint e RequestTopicServiceEndpoint definiti nel file di configurazione.

Flusso-di-lavoro-WCF-per-gli-argomenti-relativi-alle-code-del-ServiceBus-Servizio 17

In particolare, è stata usata la proprietà ServiceContractName dell'attività Receive per specificare lo spazio dei nomi di destinazione e il nome del contratto per l'endpoint di servizio ed è stata usata la proprietà Action per specificare l'intestazione azione del messaggio di richiesta come indicato nel contratto di servizio ICalculatorRequest.

È stata quindi aggiunta un'istanza di CalculatorActivity al servizio del flusso di lavoro WCF sotto BrokeredMessagePropertyActivity e sono state configurate le relative proprietà, come illustrato nella figura seguente.

Flusso-di-lavoro-WCF-per-gli-argomenti-relativi-alle-code-del-ServiceBus-Servizio 18

Successivamente, è stata aggiunta un'attività If al flusso di lavoro sotto CalculatorActivity ed è stata configurata la relativa proprietà Condition come segue:

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

È stata quindi creata un'istanza di BrokeredMessagePropertyActivity in entrambi i rami dell'attività If ed è stata aggiunta un'attività Send a ognuno di essi, come illustrato nella figura seguente.

Flusso-di-lavoro-WCF-per-gli-argomenti-relativi-alle-code-del-ServiceBus-Servizio 19

In questo modo, se l'indirizzo di risposta specificato nella proprietà ReplyTo dell'oggetto BrokeredMessageProperty in ingresso contiene la stringa "topic", la risposta verrà inviata a responsetopic. Diversamente la risposta verrà inviata a responsequeue.

In entrambi i casi, un'istanza di BrokeredMessagePropertyActivity (identificata dal nome visualizzato Set BrokeredMessage) consente di eseguire il wrapping dell'attività Send e di assegnare BrokeredMessageProperty in uscita alla raccolta di proprietà del messaggio di risposta WCF. Per eseguire questa operazione, è stato assegnato il valore della variabile outboundBrokeredMessageProperty alla proprietà BrokeredMessageProperty di entrambe le istanze di BrokeredMessagePropertyActivity, come illustrato nell'immagine seguente.

Flusso-di-lavoro-WCF-per-gli-argomenti-relativi-alle-code-del-ServiceBus-Servizio 20

È stata quindi selezionata l'attività Send nel ramo Then ed è stata configurata la relativa proprietà come segue per inviare il messaggio di risposta a responsetopic usando ResponseTopicClientEndpoint definito nel file di configurazione.

Flusso-di-lavoro-WCF-per-gli-argomenti-relativi-alle-code-del-ServiceBus-Servizio 21

In particolare, è stata usata la proprietà ServiceContractName dell'attività Send per specificare lo spazio dei nomi di destinazione e il nome del contratto per l'endpoint client, la proprietà Action per specificare l'intestazione azione del messaggio di risposta specificato nel contratto di servizio ICalculatorResponse e EndpointConfigurationName per indicare il nome dell'endpoint client definito nel file di configurazione.

In modo analogo è stata configurata l'attività Send nel ramo Else, come illustrato nella figura seguente.

Flusso-di-lavoro-WCF-per-gli-argomenti-relativi-alle-code-del-ServiceBus-Servizio 22

Nell'immagine seguente viene illustrato l'intero flusso di lavoro:

Flusso-di-lavoro-WCF-per-gli-argomenti-relativi-alle-code-del-ServiceBus-Servizio 23

Presupponendo che la soluzione sia stata configurata correttamente, è possibile procedere come indicato di seguito per eseguirne il test.

  • Per inviare un messaggio di richiesta al servizio del flusso di lavoro WCF tramite requestqueue, selezionare il pulsante di opzione Coda nel gruppo Metodi di richiesta.

  • Per inviare un messaggio di richiesta al servizio del flusso di lavoro WCF tramite requesttopic, selezionare il pulsante di opzione Argomento nel gruppo Metodi di risposta.

  • Per richiedere l'invio di un messaggio di risposta a requestqueue da parte del servizio del flusso di lavoro WCF, selezionare il pulsante di opzione Coda nel gruppo Metodi di risposta.

  • Per richiedere l'invio di un messaggio di risposta a responsetopic da parte del servizio del flusso di lavoro WCF, selezionare il pulsante di opzione Argomento nel gruppo Metodi di risposta.

Nella figura seguente viene illustrata la combinazione più interessante:

  • Il client invia un messaggio di richiesta a requesttopic.

  • Nel servizio del flusso di lavoro WCF viene letta la richiesta dalla sottoscrizione ItalyMilan per requesttopic e viene inviata la risposta a responsetopic.

  • L'applicazione client riceve il messaggio di risposta dalla sottoscrizione ItalyMilan definita in responsetopic.

Nella figura seguente sono illustrate le informazioni registrate dal servizio del flusso di lavoro WCF nell'output standard dell'applicazione console host:

Flusso-di-lavoro-WCF-per-gli-argomenti-relativi-alle-code-del-ServiceBus-Servizio 24

Nella figura seguente sono illustrate le informazioni registrate dall'applicazione client durante la chiamata:

Flusso-di-lavoro-WCF-per-gli-argomenti-relativi-alle-code-del-ServiceBus-Servizio 25

In particolare, è possibile notare quanto segue:

  1. Il messaggio di richiesta è stato inviato a requesttopic.

  2. Il messaggio di risposta è stato ricevuto da responsetopic.

  3. Il valore CorrelationId del messaggio di risposta è uguale a MessageId del messaggio di richiesta.

  4. La proprietà Label del messaggio di richiesta è stata copiata dal servizio del flusso di lavoro WCF nella proprietà Label del messaggio di risposta.

  5. Tutte le proprietà definite dall'utente sono state copiate dal servizio del flusso di lavoro WCF del messaggio di richiesta nel messaggio di risposta.

  6. Nel servizio del flusso di lavoro WCF è stata aggiunta la proprietà definita dall'utente Source alla risposta.

  7. La proprietà Area è stata aggiunta dall'azione della regola definita nella sottoscrizione ItalyMilan.

In questo articolo è stato descritto come integrare un servizio del flusso di lavoro WCF con la messaggistica negoziata del Bus di servizio e come ottenere una completa interoperabilità tra queste due tecnologie usando semplicemente le funzionalità native e un'attività personalizzata per gestire BrokeredMessageProperty. Saranno graditi commenti e suggerimenti degli utenti. Nel frattempo è possibile scaricare il codice complementare per questo articolo dalla pagina relativa agli esempi di codice del sito Web MSDN.

Mostra:
© 2015 Microsoft