PageRenderTime 252ms CodeModel.GetById 130ms app.highlight 56ms RepoModel.GetById 1ms app.codeStats 1ms

/Katmai_June2008_RC0/ServiceBrokerInterface/cs/ServiceBrokerInterface/Service.cs

#
C# | 864 lines | 503 code | 65 blank | 296 comment | 81 complexity | a15ea2206f693144c581545ee38b16da MD5 | raw file
  1//-----------------------------------------------------------------------
  2//  This file is part of the Microsoft Code Samples.
  3// 
  4//  Copyright (C) Microsoft Corporation.  All rights reserved.
  5// 
  6//This source code is intended only as a supplement to Microsoft
  7//Development Tools and/or on-line documentation.  See these other
  8//materials for detailed information regarding Microsoft code samples.
  9// 
 10//THIS CODE AND INFORMATION ARE PROVIDED AS IS WITHOUT WARRANTY OF ANY
 11//KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 12//IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
 13//PARTICULAR PURPOSE.
 14//-----------------------------------------------------------------------
 15
 16#region Using directives
 17
 18using System;
 19using System.Data;
 20using System.Data.SqlClient;
 21using System.Data.SqlTypes;
 22using System.Collections.Generic;
 23using System.Text;
 24using System.Diagnostics;
 25using System.Reflection;
 26
 27#endregion
 28
 29namespace Microsoft.Samples.SqlServer
 30{
 31    /// <remarks>
 32    /// This class represents a SQL Service Broker service endpoint.
 33    /// It is bound to an instance of the <c>SqlConnection</c> interface
 34    /// which is of type <c>System.Data.SqlClient.SqlConnection</c>.
 35    /// The <c>Service</c> class contains methods to create 
 36    /// <c>Conversation</c> objects by beginning a dialog or getting one from
 37    /// the queue.
 38    /// </remarks>
 39    public class Service
 40    {
 41        #region Fields
 42        /// <summary>
 43        /// This is the queue name associated with the service. It is 
 44        /// initilized in the constructor of the <c>Service</c> by
 45        /// querying the appropriate database management view.
 46        /// </summary>
 47        private string m_queueName;
 48
 49        /// <summary>
 50        /// The system defined contract name for echo.
 51        /// </summary>
 52        private const string EchoContractName = "http://schemas.microsoft.com/SQL/ServiceBroker/ServiceEcho";
 53
 54        private string m_appLoaderProcName;
 55        /// <value>
 56        /// If this property is non-null, the message loop executes
 57        /// the stored proc with this name while fetching the next batch of
 58        /// messages to be processed. The stored proc is an application
 59        /// specific stored proc that takes the conversation group ID as a 
 60        /// parameter and returns one or more result sets that are used for
 61        /// loading an application from database tables.
 62        /// </value>
 63        protected string AppLoaderProcName
 64        {
 65            get { return m_appLoaderProcName; }
 66            set { m_appLoaderProcName = value; }
 67        }
 68
 69        private string m_name;
 70        /// <value>
 71        /// The service name
 72        /// </value>
 73        public string Name
 74        {
 75            get { return m_name; }
 76        }
 77
 78        private int m_fetchSize;
 79        /// <value>
 80        /// The number of messages to be fetched in a database roundtrip.
 81        /// </value>
 82        public int FetchSize
 83        {
 84            get { return m_fetchSize; }
 85            set { m_fetchSize = value; }
 86        }
 87
 88        /// <value>
 89        /// Override this property if stateful behaviour of the service is desired. 
 90        /// Default returns -1.
 91        /// </value>
 92        public virtual int State
 93        {
 94            get { return -1; }
 95        }
 96
 97        /// <value>
 98        /// This is a reference to a <c>MessageReader</c> object that serves
 99        /// as a cursor for the batch of messages received from the queue.
100        /// </value>
101        private MessageReader m_reader;
102        internal MessageReader Reader
103        {
104            get { return m_reader; }
105        }
106
107        /// <value>Returns a value indicating whether there are more messages 
108        /// to be processed from the queue.</value>
109        protected bool HasMoreMessages
110        {
111            get { return m_reader.IsOpen; }
112        }
113
114        private TimeSpan m_waitforTimeout;
115        /// <value>The waitfor parameter for doing a RECEIVE while fetching new messages.</value>
116        public TimeSpan WaitforTimeout
117        {
118            get { return m_waitforTimeout; }
119            set { m_waitforTimeout = value; }
120        }
121        #endregion
122
123        #region Constructor
124        /// <summary>
125        /// The constructor instantiates a <c>Service</c> object by
126        /// querying the appropriate database management view for the given
127        /// <paramref>name</paramref>. It reads the name of the
128        /// queue associated with this <c>Service</c>.
129        /// </summary>
130        /// <param name="name">The name of the <c>Service</c> as defined
131        /// in the database</param>
132        /// <param name="connection">The database connection to be used
133        /// by this <c>Service</c></param>
134        /// <param name="transaction">The transaction to use for firing database queries
135        /// </param>
136        /// <exception cref="ArgumentException">Thrown if no such service found
137        /// in the database connected to the given connection.</exception>
138        public Service(String name, SqlConnection connection, SqlTransaction transaction)
139        {
140            if (connection.State != ConnectionState.Open)
141                throw new ArgumentException("Database connection is not open");
142
143            m_name = name;
144
145			SqlCommand cmd = connection.CreateCommand();
146			cmd.CommandText = "SELECT q.name "
147                + "FROM sys.service_queues q JOIN sys.services as s "
148                + "ON s.service_queue_id = q.object_id "
149                + "WHERE s.name = @sname";
150			cmd.Transaction = transaction;
151
152            SqlParameter param;
153            param = cmd.Parameters.Add("@sname", SqlDbType.NChar, 255);
154            param.Value = m_name;
155
156            m_queueName = (string)cmd.ExecuteScalar();
157
158            if (m_queueName == null)
159            {
160                throw new ArgumentException(
161                    "Could not find any service with the name '"
162                    + name + "' in this database.");
163            }
164
165			m_appLoaderProcName = null;
166			m_fetchSize = 0;
167            m_reader = new MessageReader(this);
168            BuildCallbackMap();
169        }
170
171        /// <summary>
172        /// The constructor is identical to the one above minus the ability to
173        /// pass in a pre-existing transaction.
174        /// </summary>
175        public Service(String name, SqlConnection connection)
176            : this(name, connection, null)
177        { }
178        #endregion
179
180        #region Virtual Methods
181        /// <summary>
182        /// <para>
183        /// This method is invoked inside the message loop for loading
184        /// the application state associated with the conversation group
185        /// being processed into the current context. It is passed an
186        /// <c>SqlDataReader</c> containing the result set(s) of executing
187        /// the stored proc <c>m_appLoaderProcName</c>. For proper functioning
188        /// it must only consume as many result sets as the stored proc is
189        /// designed to emit since the <paramref>reader</paramref> also
190        /// contains a batch of messages on the queue which are processed
191        /// later.</para>
192        /// <para>Users must override this method to perform application specific
193        /// database operations.</para>
194		/// <param name="reader">Data reader containing result set(s) of
195		/// executing the configured stored proc</param>
196		/// <param name="connection">Connection which was used for doing the RECEIVE</param>
197		/// <param name="transaction">Transaction which was used for doing the RECEIVE</param>
198        /// </summary>
199		public virtual bool LoadState(SqlDataReader reader, SqlConnection connection, SqlTransaction transaction)
200        {
201            return true;
202        }
203
204        /// <summary>
205        /// This method is invoked inside the message loop when the 
206        /// service program has finished processing a conversation group and
207        /// wishes to save the state to the database.
208        /// Users must override this method to perform application specific
209        /// database operations.
210		/// <param name="connection">Connection which was used for doing the RECEIVE</param>
211		/// <param name="transaction">Transaction which was used for doing the RECEIVE</param>
212        /// </summary>
213        public virtual void SaveState(SqlConnection connection, SqlTransaction transaction)
214        {
215        }
216
217        /// <summary>
218        /// This method provides a default implementation for dispatching messages
219        /// to the appropriate broker methods as specified in the derived class. The user
220        /// may overrride this method if attributed methods are not desired.
221        /// </summary>
222        /// <param name="message">The message received by the service</param>
223		/// <param name="connection">Connection which was used for doing the RECEIVE</param>
224		/// <param name="transaction">Transaction which was used for doing the RECEIVE</param>
225        /// <exception cref="NotImplementedException">Thrown if there is no broker method to
226        /// handle the current event</exception>
227		public virtual void DispatchMessage(
228			Message message,
229			SqlConnection connection,
230			SqlTransaction transaction)
231		{
232			if (message.Type == Message.EchoType && message.ContractName == EchoContractName)
233			{
234				EchoHandler(message, connection, transaction);
235				return;
236            }
237            MethodInfo mi;
238            BrokerMethodAttribute statefulTransition = new BrokerMethodAttribute(State, message.ContractName, message.Type);
239            BrokerMethodAttribute statefulMessageTypeTransition = new BrokerMethodAttribute(State, message.Type);
240            BrokerMethodAttribute statelessTransition = new BrokerMethodAttribute(message.ContractName, message.Type);
241            BrokerMethodAttribute statelessMessageTypeTransition = new BrokerMethodAttribute(message.Type);
242            if (m_dispatchMap.ContainsKey(statefulTransition))
243                mi = m_dispatchMap[statefulTransition];
244            else if (m_dispatchMap.ContainsKey(statefulMessageTypeTransition))
245                mi = m_dispatchMap[statefulMessageTypeTransition];
246            else if (m_dispatchMap.ContainsKey(statelessTransition))
247                mi = m_dispatchMap[statelessTransition];
248            else if (m_dispatchMap.ContainsKey(statelessMessageTypeTransition))
249                mi = m_dispatchMap[statelessMessageTypeTransition];
250            else
251            {
252                string exceptionMessage = "No broker method defined for message type '" + message.Type +
253                        "' on contract '" + message.ContractName + "'";
254                if (State != -1)
255                    exceptionMessage += " in state " + State;
256                throw new InvalidOperationException(exceptionMessage);
257            }
258            mi.Invoke(this, new object[3] { message, connection, transaction });
259            if (connection.State != ConnectionState.Open)
260            {
261                throw new ObjectDisposedException("Connection", "Method '" + mi.Name + "' closed the database connection.");
262            }
263        }
264
265		/// <summary>
266		/// This is an example implementation of the message loop. It fetches the
267		/// next conversation from the message queue, reads one message at a time
268		/// from the conversation, translates the message using the current
269		/// application object and fires the corresponding event to the application.
270		/// Application state is saved automatically whenever a new batch of messages
271		/// are fetched.
272		/// <param name="autoCommit">Set TRUE if you would like the message loop to automatically
273		/// commmit at the end of each fetch batch</param>
274		/// <param name="connection">Connection to use for doing the RECEIVE</param>
275		/// <param name="transaction">Transaction to use for doing the RECEIVE</param>
276		/// </summary>
277        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods")]
278        public virtual void Run(
279			bool autoCommit,
280			SqlConnection connection,
281			SqlTransaction transaction)
282		{
283			Message message = null;
284			Conversation currentConversation = null;
285
286			try
287			{
288				if (autoCommit == true && transaction == null)
289				{
290					transaction = connection.BeginTransaction();
291				}
292				while ((currentConversation = GetConversation(connection, transaction)) != null)
293				{
294					while ((message = currentConversation.Receive()) != null)
295					{
296						DispatchMessage(message, connection, transaction);
297					}
298					if (!HasMoreMessages)
299					{
300						SaveState(connection, transaction);
301						if (autoCommit == true)
302						{
303							transaction.Commit();
304							transaction = connection.BeginTransaction();
305						}
306					}
307				}
308				if (autoCommit == true)
309				{
310					transaction.Commit();
311					transaction = null;
312				}
313			}
314			catch (Exception e)
315			{
316				throw new ServiceException(
317					currentConversation,
318					connection,
319					transaction,
320					e);
321			}
322		}
323
324		/// <summary>
325		/// Event handler for Echo messages.
326		/// </summary>
327		/// <param name="msgReceived">The message received.</param>
328		/// <param name="connection">Connection which was used for doing the RECEIVE</param>
329		/// <param name="transaction">Transaction which was used for doing the RECEIVE</param>
330        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods")]
331        public virtual void EchoHandler(
332			Message msgReceived,
333			SqlConnection connection,
334			SqlTransaction transaction)
335		{
336			msgReceived.Conversation.Send(msgReceived, connection, transaction);
337		}
338        #endregion
339
340        #region Public Methods
341		/// <summary>
342		/// This method begins a new dialog with a remote service by invoking
343		/// the corresponding database command. 
344		/// </summary>
345		/// <param name="toServiceName">The name of the remote service to begin this
346		/// dialog with</param>
347		/// <param name="brokerInstance">The broker instance of the target broker
348		///  that has the remote service</param>
349		/// <param name="contractName">The contract to be used for this dialog
350		/// </param>
351		/// <param name="lifetime">The duration for which the dialog will be active. 
352		/// Any operation performed after the conversation has expired will result in
353		/// dialog errors.</param>
354		/// <param name="encryption">A boolean indicating whether to use encryption
355		/// on the dialog</param>
356		/// <param name="connection">The connection to use for beginning this dialog
357		/// </param>
358		/// <param name="transaction">The transaction to use for beginning this dialog
359		/// </param>
360		/// <returns>A conversation object representing this dialog</returns>
361		public Conversation BeginDialog(
362			string toServiceName,
363			string brokerInstance,
364			string contractName,
365			TimeSpan lifetime,
366			bool encryption,
367			SqlConnection connection,
368			SqlTransaction transaction)
369		{
370			return BeginDialogInternal(toServiceName, brokerInstance, contractName,
371				lifetime, encryption, null, Guid.Empty, connection, transaction);
372		}
373
374		/// <summary>
375		/// This method begins a new dialog with a remote service by invoking
376		/// the corresponding database command. It associates the dialog with
377		/// the conversation specified in <paramref>relatedConversation</paramref>
378		/// by putting them in the same conversation group.
379		/// </summary>
380		/// <param name="toServiceName">The name of the remote service to begin this
381		/// dialog with</param>
382		/// <param name="brokerInstance">The broker instance of the target broker
383		///  that has the remote service</param>
384		/// <param name="contractName">The contract to be used for this dialog
385		/// </param>
386		/// <param name="lifetime">The duration for which the dialog will be active. 
387		/// Any operation performed after the conversation has expired will result in
388		/// dialog errors.</param>
389		/// <param name="encryption">A boolean indicating whether to use encryption
390		/// on the dialog</param>
391		/// <param name="relatedConversation">The conversation that the new dialog
392		/// should be related to</param>
393		/// <param name="connection">The connection to use for beginning this dialog
394		/// </param>
395		/// <param name="transaction">The transaction to use for beginning this dialog
396		/// </param>
397		/// <returns>A conversation object representing this dialog</returns>
398		public Conversation BeginDialog(
399			string toServiceName,
400			string brokerInstance,
401			string contractName,
402			TimeSpan lifetime,
403			bool encryption,
404			Conversation relatedConversation,
405			SqlConnection connection,
406			SqlTransaction transaction)
407		{
408			return BeginDialogInternal(toServiceName, brokerInstance, contractName,
409				lifetime, encryption, relatedConversation, Guid.Empty, connection, transaction);
410		}
411
412		/// <summary>
413		/// This method begins a new dialog with a remote service by invoking
414		/// the corresponding database command. It associates the dialog with
415		/// the specified conversation group.
416		/// </summary>
417		/// <param name="toServiceName">The name of the remote service to begin this
418		/// dialog with</param>
419		/// <param name="brokerInstance">The broker instance of the target broker
420		///  that has the remote service</param>
421		/// <param name="contractName">The contract to be used for this dialog
422		/// </param>
423		/// <param name="lifetime">The duration for which the dialog will be active. 
424		/// Any operation performed after the conversation has expired will result in
425		/// dialog errors.</param>
426		/// <param name="encryption">A boolean indicating whether to use encryption
427		/// on the dialog</param>
428		/// <param name="groupId">The conversation group Id that the new dialog
429		/// should belong to</param>
430		/// <param name="connection">The connection to use for beginning this dialog
431		/// </param>
432		/// <param name="transaction">The transaction to use for beginning this dialog
433		/// </param>
434		/// <returns>A conversation object representing this dialog</returns>
435		public Conversation BeginDialog(
436			string toServiceName,
437			string brokerInstance,
438			string contractName,
439			TimeSpan lifetime,
440			bool encryption,
441			Guid groupId,
442			SqlConnection connection,
443			SqlTransaction transaction)
444		{
445			return BeginDialogInternal(toServiceName, brokerInstance, contractName,
446				lifetime, encryption, null, groupId, connection, transaction);
447		}
448
449		/// <summary>
450		/// This method returns the next active conversation on the message queue. If
451		/// associated <c>MessageReader</c> object is empty, a new batch of messages
452		/// are fetched from the database.
453		/// </summary>
454		/// <param name="connection">The connection to use for fetching message batch
455		/// </param>
456		/// <param name="transaction">The transaction to use for fetching message batch
457		/// </param>
458		/// <returns>A <c>Conversation</c> object on which <c>Receive</c> may be invoked
459		/// to get the messages received on that conversation.</returns>
460		public Conversation GetConversation(
461			SqlConnection connection,
462			SqlTransaction transaction)
463		{
464			if (!m_reader.IsOpen)
465			{
466				FetchNextMessageBatch(null, connection, transaction);
467			}
468			return m_reader.GetNextConversation();
469		}
470
471		/// <summary>
472		/// This method blocks (or times out) until the specified conversation is
473		/// available on the message queue.
474		/// </summary>
475		/// <param name="conversation">The conversation to fetch from the queue</param>
476		/// <param name="connection">The connection to use for fetching message batch
477		/// </param>
478		/// <param name="transaction">The transaction to use for fetching message batch
479		/// </param>
480		/// <returns>The conversation available on the queue</returns>
481		/// <exception cref="InvalidOperationException">Thrown if attempted to call
482		/// this method when there is some other conversation active.</exception>
483		public Conversation GetConversation(
484			Conversation conversation,
485			SqlConnection connection,
486			SqlTransaction transaction)
487		{
488			Conversation nextConv;
489			if (m_reader.IsOpen)
490			{
491				nextConv = m_reader.GetNextConversation();
492				if (nextConv.Handle == conversation.Handle)
493					return nextConv;
494				else
495					throw new InvalidOperationException("Cannot get conversation '" +
496						conversation.Handle + "' while there are still more messages to be processed.");
497			}
498
499			FetchNextMessageBatch(conversation, connection, transaction);
500			nextConv = m_reader.GetNextConversation();
501			Debug.Assert(nextConv == null || nextConv.Handle == conversation.Handle);
502			return nextConv;
503		}
504		#endregion
505
506        #region Private methods
507        /// <summary>
508        /// This is an internal method that is called by the various <c>BeginDialog</c> 
509        /// methods. It invokes the BEGIN DIALOG T-SQL command and creates a new
510        /// Conversation object wrapping the returned conversation handle.
511        /// </summary>
512        /// <param name="toServiceName">The name of the remote service to connect
513        /// to</param>
514		/// <param name="brokerInstance">The broker instance of the target broker
515		///  that has the remote service</param>
516        /// <param name="contractName">The contract to be used for this dialog
517        /// </param>
518        /// <param name="lifetime">The duration for which the dialog will be active. 
519        /// Any operation performed after the conversation has expired will result in
520        /// dialog errors.</param>
521        /// <param name="encryption">A boolean indicating whether to use encryption
522        /// on the dialog</param>
523        /// <param name="conversation">The conversation that the new dialog
524        /// should be related to</param>
525        /// <param name="groupId">The conversation group Id that the new dialog
526        /// should belong to</param>
527		/// <param name="connection">The connection to use for beginning this dialog
528		/// </param>
529		/// <param name="transaction">The transaction to use for beginning this dialog
530		/// </param>
531		/// <returns>A conversation object representing this dialog</returns>
532		private Conversation BeginDialogInternal(
533			string toServiceName,
534			string brokerInstance,
535			string contractName,
536			TimeSpan lifetime,
537			bool encryption,
538			Conversation conversation,
539			Guid groupId,
540			SqlConnection connection,
541			SqlTransaction transaction)
542		{
543			SqlParameter param;
544			SqlCommand cmd = connection.CreateCommand();
545
546			StringBuilder query = new StringBuilder();
547			if (brokerInstance != null)
548			{
549				query.Append("BEGIN DIALOG @ch FROM SERVICE @fs TO SERVICE @ts, @bi ON CONTRACT @cn WITH ENCRYPTION = ");
550				param = cmd.Parameters.Add("@bi", SqlDbType.NVarChar, brokerInstance.Length);
551				param.Value = brokerInstance;
552			}
553			else
554			{
555				query.Append("BEGIN DIALOG @ch FROM SERVICE @fs TO SERVICE @ts ON CONTRACT @cn WITH ENCRYPTION = ");
556			}
557
558			if (encryption)
559				query.Append("ON ");
560			else
561				query.Append("OFF ");
562			if (conversation != null)
563			{
564				query.Append(", RELATED_CONVERSATION = @rch ");
565				param = cmd.Parameters.Add("@rch", SqlDbType.UniqueIdentifier);
566				param.Value = conversation.Handle;
567			}
568			else if (groupId != Guid.Empty)
569			{
570				query.Append(", RELATED_CONVERSATION_GROUP = @rcg ");
571				param = cmd.Parameters.Add("@rcg", SqlDbType.UniqueIdentifier);
572				param.Value = groupId;
573			}
574			if (lifetime > TimeSpan.Zero)
575			{
576				query.Append(", LIFETIME = ");
577				query.Append((long)lifetime.TotalSeconds);
578				query.Append(' ');
579			}
580
581			param = cmd.Parameters.Add("@ch", SqlDbType.UniqueIdentifier);
582			param.Direction = ParameterDirection.Output;
583			param = cmd.Parameters.Add("@fs", SqlDbType.NVarChar, 255);
584			param.Value = m_name;
585			param = cmd.Parameters.Add("@ts", SqlDbType.NVarChar, 255);
586			param.Value = toServiceName;
587			param = cmd.Parameters.Add("@cn", SqlDbType.NVarChar, 128);
588			param.Value = contractName;
589
590			cmd.CommandText = query.ToString(); ;
591			cmd.Transaction = transaction;
592
593			cmd.ExecuteNonQuery();
594
595			param = cmd.Parameters["@ch"] as SqlParameter;
596			Guid handle = (Guid)param.Value;
597			Conversation dialog = new Conversation(this, handle);
598			return dialog;
599		}
600
601        /// <summary>
602        /// <para>
603        /// This private method is called to fetch a new set of messages from the
604        /// queue. If the <paramref>conversation</paramref> parameter is non-null, we execute
605        /// the following database command:
606        /// <code>
607        /// RECEIVE conversation_group_id, conversation_handle,message_sequence_number, 
608        ///			service_name, service_contract_name, message_type_name, validation,
609        ///			message_body
610        /// FROM m_queuename
611        /// WHERE conversation_handle=conversation.Handle
612        /// </code>
613        /// 
614        /// If <paramref>conversation</paramref> is null, we check if a stored proc is set or 
615        /// not and then invoke the appropriate database command. If no stored proc is
616        /// set, then the batch of commands we use is:
617        /// <code>
618        /// RECEIVE conversation_group_id, conversation_handle,message_sequence_number, 
619        ///			service_name, service_contract_name, message_type_name, validation,
620        ///			message_body
621        /// FROM m_queuename
622        /// </code>
623        /// If a stored proc is set, then we use:
624        /// <code>
625        /// DECLARE @cgid UNIQUEIDENTIFIER;
626        /// IF @cgid IS NOT NULL
627        /// BEGIN
628        ///		GET CONVERSATION GROUP @cgid FROM m_queuename;
629        ///		EXEC m_AppLoaderProcName (@cgid);
630        ///		RECEIVE conversation_group_id, conversation_handle,message_sequence_number, 
631        ///			service_name, service_contract_name, message_type_name, validation,
632        ///			message_body
633        ///		FROM m_queuename
634        ///		WHERE conversation_group_id=@cgid;
635        /// END
636        /// </code>
637        /// </para>
638        /// 
639        /// <para>If the stored proc was specified, then we send the <c>SqlDataReader</c>
640        /// returned to the <c>Load</c> method to load the application state associated
641        /// with the current conversation group. The <c>Load</c> method consumes all the 
642        /// result sets returned by the user's stored proc but leaves the result set
643        /// returned by the RECEIVE. After loading the application state, we initialize
644        /// the message reader with the <c>SqlDataReader</c>, which can iterate over the 
645        /// batch of messages received.
646        /// </para>
647        /// </summary>
648        /// <param name="conversation">If set to a valid conversation, then we only fetch
649        /// messages belonging to that conversation</param>
650		/// <param name="connection">The connection to use for issuing the T-SQL batch for
651		/// fetching messages</param>
652		/// <param name="transaction">The transaction to use issuing the T-SQL batch for
653		/// fetching messages</param>
654		private void FetchNextMessageBatch(
655			Conversation conversation,
656			SqlConnection connection,
657			SqlTransaction transaction)
658		{
659			SqlCommand cmd;
660			if (conversation != null || m_appLoaderProcName == null)
661			{
662				cmd = BuildReceiveCommand(conversation, connection, transaction);
663
664				SqlDataReader dataReader = cmd.ExecuteReader();
665
666				m_reader.Open(dataReader);
667			}
668			else if (m_appLoaderProcName != null)
669			{
670				cmd = BuildGcgrCommand(connection, transaction);
671				SqlDataReader dataReader = cmd.ExecuteReader();
672
673				if (!LoadState(dataReader, connection, transaction))
674				{
675					dataReader.Close();
676					return;
677				}
678
679				m_reader.Open(dataReader);
680			}
681		}
682
683		private SqlCommand BuildReceiveCommand(
684			Conversation conversation,
685			SqlConnection connection,
686			SqlTransaction transaction)
687		{
688			SqlParameter param;
689			SqlCommand cmd = connection.CreateCommand();
690			cmd.Transaction = transaction;
691			StringBuilder query = new StringBuilder();
692
693			if (m_waitforTimeout != TimeSpan.Zero)
694				query.Append("WAITFOR(");
695			query.Append("RECEIVE ");
696
697			if (m_fetchSize > 0)
698				query.Append("TOP(" + m_fetchSize + ") ");
699
700			query.Append("conversation_group_id, conversation_handle, " +
701						 "message_sequence_number, service_name, service_contract_name, " +
702						 "message_type_name, validation, message_body " +
703						 "FROM ");
704			query.Append(m_queueName);
705			if (conversation != null)
706			{
707				query.Append(" WHERE conversation_handle = @ch");
708				param = cmd.Parameters.Add("@ch", SqlDbType.UniqueIdentifier);
709				param.Value = conversation.Handle;
710			}
711			if (m_waitforTimeout < TimeSpan.Zero)
712			{
713				query.Append(")");
714				cmd.CommandTimeout = 0;
715			}
716			else if (m_waitforTimeout > TimeSpan.Zero)
717			{
718				query.Append("), TIMEOUT @to");
719				param = cmd.Parameters.Add("@to", SqlDbType.Int);
720				param.Value = (int)m_waitforTimeout.TotalMilliseconds;
721				cmd.CommandTimeout = 0;
722			}
723			cmd.CommandText = query.ToString();
724			return cmd;
725		}
726
727		private SqlCommand BuildGcgrCommand(
728			SqlConnection connection,
729			SqlTransaction transaction)
730		{
731			SqlParameter param;
732			SqlCommand cmd = connection.CreateCommand();
733			cmd.Transaction = transaction;
734			StringBuilder query = new StringBuilder(
735				"DECLARE @cgid UNIQUEIDENTIFIER;\n"
736			);
737
738			if (m_waitforTimeout != TimeSpan.Zero)
739				query.Append("WAITFOR(");
740			query.Append("GET CONVERSATION GROUP @cgid FROM " + m_queueName);
741			if (m_waitforTimeout < TimeSpan.Zero)
742			{
743				query.Append(")");
744				cmd.CommandTimeout = 0;
745			}
746			else if (m_waitforTimeout > TimeSpan.Zero)
747			{
748				query.Append("), TIMEOUT @to");
749				param = cmd.Parameters.Add("@to", SqlDbType.Int);
750				param.Value = (int)m_waitforTimeout.TotalMilliseconds;
751				cmd.CommandTimeout = 0;
752			}
753			query.Append(";\nIF @cgid IS NOT NULL\nBEGIN\nEXEC " + m_appLoaderProcName + " @cgid;\n");
754			query.Append("RECEIVE ");
755
756			if (m_fetchSize > 0)
757				query.Append("TOP(" + m_fetchSize + ") ");
758
759			query.Append("conversation_group_id, conversation_handle, " +
760						 "message_sequence_number, service_name, service_contract_name, " +
761						 "message_type_name, validation, message_body " +
762						 "FROM ");
763			query.Append(m_queueName);
764			query.Append(" WHERE conversation_group_id = @cgid;\nEND");
765			cmd.CommandText = query.ToString();
766			return cmd;
767		}
768		#endregion
769
770		#region Message Dispatcher
771		private Dictionary<BrokerMethodAttribute, MethodInfo> m_dispatchMap;
772
773		private void BuildCallbackMap()
774		{
775			Type t = GetType();
776			m_dispatchMap = new Dictionary<BrokerMethodAttribute, MethodInfo>();
777			MethodInfo[] methodInfoArray = t.GetMethods(BindingFlags.Public | BindingFlags.Instance);
778			foreach (MethodInfo methodInfo in methodInfoArray)
779			{
780				object[] attributes = methodInfo.GetCustomAttributes(typeof(BrokerMethodAttribute), true);
781				foreach (BrokerMethodAttribute statefulTransition in attributes)
782				{
783					BrokerMethodAttribute statelessTransition =
784						new BrokerMethodAttribute(statefulTransition.Contract, statefulTransition.MessageType);
785					if (m_dispatchMap.ContainsKey(statefulTransition) ||
786						m_dispatchMap.ContainsKey(statelessTransition))
787					{
788						string exceptionMessage = "Method '" + methodInfo.Name +
789							"' redefines a handler for message type '" + statefulTransition.MessageType + "'";
790						if (statefulTransition.State != -1)
791							exceptionMessage += " in state " + statefulTransition.State;
792
793						throw new NotSupportedException(exceptionMessage);
794					}
795
796					m_dispatchMap[statefulTransition] = methodInfo;
797				}
798			}
799		}
800		#endregion
801
802        #region MessageReader inner class
803        internal class MessageReader
804        {
805            private Service m_svc;
806            private SqlDataReader m_dataReader;
807            private Message m_curMsg;
808
809            public MessageReader(Service svc)
810            {
811                m_svc = svc;
812				m_dataReader = null;
813				m_curMsg = null;
814            }
815
816            public void Open(SqlDataReader dataReader)
817            {
818                m_dataReader = dataReader;
819                AdvanceCursor();
820            }
821
822            public bool IsOpen
823            {
824                get
825                {
826                    return m_curMsg != null;
827                }
828            }
829
830            public Message Read(Conversation conversation)
831            {
832                if (m_curMsg == null || m_curMsg.Conversation.Handle != conversation.Handle)
833                    return null;
834
835                Message result = m_curMsg;
836                AdvanceCursor();
837                return result;
838            }
839
840            public Conversation GetNextConversation()
841            {
842                if (m_curMsg == null)
843                    return null;
844
845                return m_curMsg.Conversation;
846            }
847
848            private void AdvanceCursor()
849            {
850                if (m_dataReader.Read())
851                {
852                    m_curMsg = new Message();
853                    m_curMsg.Read(m_dataReader, m_svc);
854                }
855                else
856                {
857                    m_dataReader.Close();
858                    m_curMsg = null;
859                }
860            }
861        }
862        #endregion
863    }
864}