PageRenderTime 38ms CodeModel.GetById 9ms RepoModel.GetById 0ms app.codeStats 0ms

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