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