Developing a Transactional BizTalk Adapter Using the Microsoft Base Adapter Classes.... 3
Writing a Microsoft BizTalk Server 2006 adapter can be a daunting task. A significant part of the initial difficulty involves understanding the flow of messages into and out of an adapter. This complexity includes which interfaces are involved, the sequence of method calls, which components are responsible for implementing which interface methods, and which functionality is implemented by the adapter or by the BizTalk Messaging Engine.
During the development process you decide which of the following primary approaches to use in writing your adapter:
· You can use the BizTalk Adapter Framework and its public APIs. The Adapter Framework provides administrative interfaces (property page browser), tools (the Add Adapter Metadata Wizard), development restrictions, and a runtime environment that simplifies development. The Adapter Framework also provides a set of standard interfaces to ensure that all design-time configuration for any adapter is confined to the BizTalk Server Administration console and BizTalk Explorer in Microsoft Visual Studio 2005.
· A simpler choice is to use the BizTalk Server SDK Base Adapter classes. The source code ships as part of the BizTalk Server 2006 SDK as a sample project from which to derive your adapter. Here most of the primary adapter functionality is implemented for you, so you need to write less code than you would if you used the Adapter Framework. The tradeoff is that you have to do more initial research during the design phase to understand which Base Adapter classes to inherit from, which classes to use, and which code implements which functionality. After you understand what you need to do and what the Base Adapter classes give you for free, you can more efficiently develop your adapter.
Understanding how much functionality is provided for you by the Base Adapter classes and what you are responsible for implementing is a primary motivation behind this document. This document's intent is to support adapter developers by providing an explanation of how to implement different functionalities when using these classes. It should help you to understand which parts of your adapter the Base Adapter classes implement on your behalf and which parts you need to write.
This document contains some advanced adapter concepts. For supporting information about basic adapter concepts, see the information under "Developing Custom Adapters" in BizTalk Server 2006 Help.
To demonstrate development concepts, we mainly refer to the transactional adapter SDK sample. Occasionally we also refer to the non-transactional file adapter SDK sample to contrast or expand upon implementation details. You can find both of these samples in the SDK\Samples\Adapter Development folder. We recommend that you have the applicable project open in Visual Studio so that you can follow along with the concepts discussed here.
One of the main areas of focus for this document is the fundamental concepts of sending and receiving messages for one-way adapters (send or receive). The more complex two-way adapter interactions of Solicit-Response and Request-Response are just variations of these two basic constructs, but they differ in their use of transactions.
You cannot have transacted Request-Response adapters due to a fundamental limitation of the BizTalk Server architecture. To comprehend why this is not feasible let's examine the order of events and transactions that occur in a Request-Response message exchange involving an orchestration:
1. The request message arrives at the receive adapter from an external requestor.
2. The receive adapter translates the data into a BizTalk Server message and submits it to the Messaging Engine.
3. In a transactional operation TXN1, the Messaging Engine submits the message into the MessageBox database and commits transaction TXN1.
4. The orchestration (or alternatively a send port) with an active subscription to the message type gets the message. It processes the message and creates a response message, which it submits to the MessageBox database using another transaction, TXN2.
5. The adapter receives the response message from BizTalk Server and sends the response message to the original external requestor. After successful transmission of the response message, the adapter acknowledges its successful processing of that message by executing a Delete of the response message against BizTalk Server.
To get past step 3, the transaction surrounding the insert of the request message into the MessageBox database must be committed. If this transaction is not committed, then step 3 cannot complete because the data is not actually in the MessageBox database until the transaction commits. If step 4 fails and rolls back TXN2, then TXN1 will not roll back because it has already been committed. This leaves an orphaned request message in the database without a corresponding response message. You want either both the request and response messages to be processed, or none of them to be processed. With separate transactions in this case you cannot guarantee these processing results. Thus Request-Response adapters cannot be transactional.
On the send side the Solicit-Response interaction is a subtly different story. Logical ports used in an orchestration are physically implemented as internal queues inside of BizTalk Server. A message coming into a receive port goes into an internal queue. The orchestration receives the message from that queue (receive location), processes it, and writes it out to another queue (send port) by using a transaction. All of these steps can be done under one transaction. Unlike the Request-Response interaction, which needs two separate transactions to complete its processing, the Solicit-Response interaction can do all its processing with one transaction. This permits two-way transactional communication. You create the transaction in the adapter, make a request and receive a response against an external system, and then delete the message from BizTalk Server, all under one transaction. For example, the adapter could execute a stored procedure on an external database and then execute the SubmitResponse and Delete operations on BizTalk Server. All this can be done under the same transaction. SubmitResponse and Delete should always be called from the same batch so they are done under the same transaction. This is true whether that transaction originates in the send adapter or whether BizTalk Server creates it behind the scenes.
The following diagram shows a Request-Response interaction on the receive side and a Solicit-Response interaction on the send side. It also shows the internal BizTalk Server queues and the interaction of the receive and send adapters with these queues. There is always a transaction when a message is submitted to, or received from, a queue.
· (RR1) Request side of a Request-Response interaction. The transaction used in RR1 is for the receive operation where the BizTalk Server message is placed into the inbound queue. This transaction is internal and BizTalk Server creates it automatically when the message is submitted to the MessageBox database.
· (SR1) Solicit-Response interaction. The transaction used in SR1 is an explicit MSDTC transaction created by the adapter. Explicit MSDTC transactions are needed to interact with external systems. This transaction is used to acknowledge the message receipt from an internal queue, send the message to an external server for processing, accept the response from the external server, and submit the response back into an internal BizTalk Server queue, all using the same transaction.
· (RR2) Response side of a Request-Response interaction. The new transaction used in the Response side of RR2 is the acknowledgment of the message transmitted to the external client as a response. The acknowledgment of a message from an internal queue in BizTalk Server means to delete the message. Like the transaction used in RR1, this transaction is implicitly created by BizTalk Server.
The use of different transactions in RR1 and RR2 shows that, given the existence of the queues in the current architecture, the Request-Response interaction can never meaningfully involve an explicit MSDTC transaction or a single implicit transaction.
The use of transactions within a BizTalk Server adapter can be somewhat confusing. The presence of a transaction allows BizTalk Server to guarantee that a message batch is delivered once and only once to BizTalk Server. This is a positive attribute in a distributed message processing environment such as BizTalk Server.
When using the term "transactional" adapter specifically we are referring to adapters that explicitly create their own Microsoft Distributed Transaction Coordinator (MSDTC) transaction and submit it to BizTalk Server. (Throughout this document a "transaction", or "explicit transaction", refers to an MSDTC transaction.) An explicit transaction is required by a transactional adapter when it needs to involve an external system's processing within the scope of the work done by BizTalk Server. A resource manager is a transactional-aware entity, such as SQL Server, MQSeries, or Message Queuing (also known as MSMQ), which speaks the OLETX transaction protocol with MSDTC. The transaction logically connects a BizTalk Server operation and ancillary operations executed by an external resource manager into one atomic operation. For example, you may want to both submit a message to BizTalk Server (receive side of your adapter) and write a part of it into an MSMQ message and send it (send side of your adapter) to a transactional MSMQ message queue.
A non-transactional adapter does not explicitly create an MSDTC transaction. Rather it implicitly leaves it up to BizTalk Server to handle that task.
To better understand explicit vs. implicit transactions we need to look at the notion of a batch. All messages are submitted to BizTalk Server using a batch. Batches are linked to transactions in that every batch runs in the context of a transaction inside of BizTalk Server. It is the origin of this transaction that is a legitimate point of confusion for some developers. The transaction used is either implicitly created by BizTalk Server, or explicitly created by the adapter. In the implicit case the non-transactional adapter passes in a NULL transaction object parameter when submitting the batch and BizTalk Server creates an internal transaction. This private transaction is not accessible to the adapter. It is committed or aborted by BizTalk Server based upon the success or failure of all its contained messages being submitted into the MessageBox database. In the explicit scenario, the transactional adapter creates and passes in the transaction to BizTalk Server. The transaction remains accessible to the adapter for additional enlistment with other MSDTC resource managers. The outcome of the transaction (commit or abort) is now determined by the adapter and its logic, and not by BizTalk Server. The adapter merely calls DTCConfirmCommit to inform BizTalk Server of its decision.
An example of a non-transactional adapter is the File adapter because none of its operations are transactional. In contrast, the SQL adapter's operations are transactional because it requires access to an external transaction object.
A transaction's role differs depending upon whether it is used in a BizTalk Server send or receive operation. When sending, a transaction is used to ensure the resource manager successfully receives the message and can guarantee it has been delivered. For receive operations it is used when submitting incoming messages into BizTalk Server.
For performance purposes it is always better to allow the resource manager (SQL Server in the case of BizTalk Server message submission) to use its own optimized, internal transactional processing. The overhead of creating and passing around an explicit MSDTC transaction is significant. Under load with multiple transactional adapter threads concurrently talking to SQL Server through explicit MSDTC transactions you will see a drop in throughput as opposed to using implicit transactions. The "D" in MSDTC stands for "Distributed," meaning the transaction will be shared or distributed across more than one transactional resource manager during its lifetime. Thus if you are not distributing the transaction, and are not using an external resource manager, there is no need for an explicit MSDTC transaction. To use it in that case would just lower performance and make the adapter code more complex. If the external system supports the MSDTC OLETX protocol, you should use transactions and let MSDTC do the work for you.
The less you are required to know about the low-level BizTalk Server interfaces the simpler it is to develop and maintain an adapter solution. The Base Adapter modules are source helper classes created with the sole purpose of making it easier for developers to code a BizTalk adapter. These classes abstract out and wrap the lower-level adapter functionality. In some cases the code may not meet your needs exactly and thus you cannot derive your functionality from them as is. In those situations it may be best to create your own classes by doing a cut-and-paste operation into your class module and modifying the code as needed. The transactional adapter sample takes this approach in certain spots.
The BizTalk Server functionality wrapped by these classes lies in native libraries that are called by using a .NET interop proxy from managed code. Keep in mind during your development process that management and cleanup of instance memory is an important issue to understand in mixed-mode scenarios like this.
The Base Adapter classes were used in the development of the native adapters that ship with BizTalk Server 2006. With the exception of the SubmitDirect sample, all SDK samples use the Base Adapter classes. When moving from BizTalk Server 2004 to BizTalk Server 2006, support was added to this class surrounding transactional receive and transmit operations. This updated version of the Base Adapter classes removed a layer of indirection for handling batches. This has resulted in less complex code in fewer lines of execution within batch processing, both transactional and normal.
The following figure shows the class structure surrounding the Base Adapter and its components. For the sake of this discussion the classes are broken into primary and secondary categories. The primary classes are those with which you do most of your main adapter work. The secondary classes are more generic in nature and assist with auxiliary functionality.
These classes are the primary ones you will use in your adapter project. They pertain mainly to endpoints, batches, and send or receive functionality. As you can see from the preceding figure, the classes you use and objects you create for a receive adapter (receiver and receiver endpoint) are different from those for a send adapter (transmitter, transmitter batch, and transmitter endpoint).
The primary classes are as follows:
· Adapter (Adapter.cs). This is an abstract root class for both send and receive adapters. Using the abstract modifier at the class level means it cannot be instantiated and can only be used as a base class for subclasses. With the exception of batch and design-time classes, most of the Base Adapter classes that relate directly to the actual receive and send processing of messages are defined as abstract.
Adapter implements IBTTransport to identify the adapter when BizTalk Server requests its information (name, version, CLSID, description, and transport type). It implements IBTTransportControl for initialization and termination processing for in-process adapters. (For more information, see the "Termination Management" section later in this document.) The Adapter class also implements the optional IPersistPropertyBag interface with which the adapter receives handler configuration data from BizTalk Server during initialization.
In the sample, both Receiver (receive functionality) and AsyncTransmitter (send functionality) derive from Adapter.
· Receiver (Receiver.cs). This is an abstract base class for all receive adapters. It derives from Adapter and provides a standard implementation of a receive adapter's interfaces. It keeps a hash table of active receive endpoints (the ReceiverEndpoint class, which is discussed later) that correspond to receive locations. It implements IBTTransportConfig to manage adding, updating, and removing endpoints. It implements IDisposable to free native resources when the adapter shuts down.
When your receive adapter derives from Receiver your class is a singleton object. That means no matter how many client threads call an API or method to create an instance of your object, the result is one and only one shared instance of the same object. All the issues that surround object serialization, such as throughput, blocking, and synchronization of shared resources, need to be taken into account in your adapter development process. The primary reason for using a singleton architecture is that, as an optimization, BizTalk Server allows messages for different endpoint locations to be submitted in the same batch. By having a single instance of a receive adapter its logic can extract the different endpoints from the messages and ensure they are sent optimally and correctly to their destinations. The Receiver class acts as a class factory for instances of the ReceiverEndPoint class.
In the sample, the TransactionalReceiver class is the receive adapter. This inherits from Receiver, and BizTalk Server loads it at startup time when it instantiates all receive adapters.
· AsyncTransmitter (AsyncTransmitter.cs). This is the base class for all send adapters, deriving from Adapter and implementing IBTBatchTransmitter. The main method for this class is GetBatch, which provides an AsyncTransmitterBatch (discussed later) message batch to the BizTalk Messaging Engine when it has messages to be transmitted by the adapter. AsyncTransmitter acts as a class factory for instances of the TransmitReceiverEndpoint class. This class also contains endpoint management methods along with termination processing (see the "Termination Management" section). It implements IDisposable to free native resources when the adapter shuts down. Like the receive adapter, when your send adapter derives from AsyncTransmitter it is a singleton object.
In the sample, the TransactionalTransmitter class is the send adapter. This inherits from AsyncTransmitter, and BizTalk Server loads it at run time when it instantiates the send adapter. Send adapters use the "lazy creation" paradigm by default for performance purposes. A send adapter is instantiated only at the point when messages need to be transmitted to endpoints linked to that adapter.
· ReceiverEndpoint (ReceiverEndpoint.cs). You derive your own receiver endpoint class from the abstract ReceiverEndpoint class and implement its methods to receive messages from a specific receive location. You code the receive operation (listening or polling) particular to your adapter in this class.
The singleton Receiver class uses the ReceiverEndpoint class to track its active endpoints. For each active endpoint, Receiver creates an instance of the ReceiverEndpoint class and calls its Open method. You implement this method to include the receipt of the message and its submission into BizTalk Server. The Receiver class holds a reference to an instance of the ReceiverEndpoint class by using a Dictionary object indexed on URI. Thus if there are 10 different FTP receive locations there will be 10 instances of your derived ReceiverEndpoint class serviced by one (FTP) Receiver adapter class. If the administrator updates or deletes a receive location, the BizTalk Server Administration console calls the Update or Dispose method to carry out the corresponding operation.
In the sample, TransactionalReceiverEndpoint derives from ReceiverEndpoint. This derived class contains all the code related to receiving a transactional message. In the sample, a single timer does the polling. You would implement your own functionality to receive a message specific to your protocol and architecture. Receiving the message (in the sample, pulling data from the Northwind database), creating the IBaseMessage object, and submitting it to BizTalk Server is done in the SubmitBatch method. This is discussed in detail later in the "Receive Side of the Adapter" section.
· AsyncTransmitterBatch (AsyncTransmitterBatch.cs). This class is the implementation of IBTTransmitterBatch and does not derive from the Batch class like all other batch subclasses. Your code subclasses this class and provides an instance of this object to the BizTalk Messaging Engine. The Messaging Engine in turn uses this object to send messages back to your adapter for transmission. This class allows these messages to come automatically into your endpoint class. Be careful not to confuse this "outgoing" batch, which the adapter provides for the BizTalk Messaging Engine to accept outgoing messages for transmission, with the "incoming" batch (Batch), which the transport proxy provides to the adapter on behalf of the BizTalk Messaging Engine to accept incoming messages for submission to BizTalk Server.
Source code comments inside the AsyncTransmitterBatch.cs code module mention routing based upon the OutboundTransmitLocation property. This is incorrect because that property does not exist. Rather, the OutboundLocation member is used to route the messages.
The Worker method is the high-level call where message transmission begins. You can override this method for specific batch processing, such as sorting the batch into smaller batches to be processed individually at a more granular level. This method creates a TransmitResponseBatch batch and calls ProcessMessage in a loop on each endpoint object for each message in the batch. ProcessMessage is an abstract method that you override in your endpoint class to execute the transmission operation for a single message. In your implementation of the AsyncTransmitterEndpointBatch class you call the AsyncTransmitterEndpoint.ProcessMessage method once for each endpoint. Upon return from this method BizTalk Server deletes the message from its queue if this is a one-way send. If ProcessMessage returns a response message, it is submitted back into BizTalk Server.
The following code from the AsyncTransmitterBatch.Worker method shows how to use ProcessMessage and take the correct subsequent steps based upon success or failure of that call:
foreach (IBaseMessage message in this.messages)
{
AsyncTransmitterEndpoint endpoint = null;
try
{
//Get appropriate endpoint for the message. Should always be non-//null. GetEndPoint copies the message context into a new message //context and returns the context.OutboundTransportLocation //property.
endpoint = (AsyncTransmitterEndpoint)asyncTransmitter.GetEndpoint(message);
// Ask the endpoint to process the message.
IBaseMessage responseMsg = endpoint.ProcessMessage(message
//Message was sent and processed successfully by the endpoint to tell the
//BizTalk Messaging Engine we are finished with this message.
batch.DeleteMessage(message);
if (responseMsg != null)
{
batch.SubmitResponseMessage(message, responseMsg);
}
}
catch (AdapterException e)
{
HandleException(e, batch, message);
}
catch (Exception e)
{
HandleException(new ErrorTransmitUnexpectedClrException(e.Message), batch, message);
}
finally
{
//If we have a valid endpoint and we are not going to reuse the //endpoint remove the AsyncTransmitterEndpoint endpoint.
if (endpoint != null && endpoint.ReuseEndpoint == false)
{
endpoint.Dispose();
endpoint = null;
}
}
}
The transactional adapter does not derive from the AsyncTransmitterBatch class. Its functionality instead exists within TransactionalAsyncBatch.cs in a modified form. The developer of the sample felt initially that most of the code in AsyncTransmitterBatch did not meet the sample's requirements. Introducing transactions slightly alters the control flow so the default implementation of this class may not work. As a workaround he implemented the sample's functionality directly from the IBTTransmitterBatch interface. However, after later revisiting this class he stated that using AsyncTransmitterBatch would probably have been a better approach, and that a developer can probably use the AsyncTransmitterBatch class as is in almost all cases.
Because of the direct implementation of IBTTramsmitterBatch, the ProcessMessage method is not used in the transactional adapter sample. This can be a common point of confusion for those trying to understand the Base Adapter classes. Instead, all the message transmission occurs in the SendMessage method. If you would like an example of how to implement ProcessMessage, then SendMessage will help you because the actual send work occurs here. Alternatively you can refer to the ProcessMessage implementation in the sample file adapter's DotNetFileTransmitterEndpoint.cs file.
· AsyncTransmitterEndpoint (AsyncTransmitterEndpoint.cs). This is an abstract base class for send adapter endpoints. It contains the three class definitions of EndpointParameters, DefaultEndpointParameters, and AsyncTransmitterEndpoint. These classes are used directly by the AsyncTransmitter and AsyncTransmitterBatch classes, and are typically not used by your adapter's derived classes.
The three endpoint classes are as follows:
· EndpointParameters is a base class that represents the endpoint location where the message is to be sent. The URI of this location is stored in the OutboundLocation member. When a message is sent to the adapter for transmission, by default the adapter uses the OutboundLocation member of this class to decide where to send the message. If you do not want your adapter to use this value to send a message, then override the CreateEndPointParameters method in your derived endpoint class and use your custom location property. Additionally there may be times when the OutboundLocation alone does not adequately define the endpoint and an additional part of the message context might need to be used for disambiguation.
BizTalk Server enables you to configure multiple send ports using the same URI and does not provide for unique values to differentiate the URIs. For example, take the case of Enterprise Single Sign-On. (For more information see "Enterprise Single Sign-On" in BizTalk Server 2006 Help.) Given an affiliate application ID and a corresponding ticket acquired by a given username and password, the adapter does a lookup in the SSO database for a particular FTP URI folder on a remote system. While the FTP URI remains constant, the ticket changes depending upon the user's credentials. This ticket allows further refinement of the send URI by possibly returning different folders on the same FTP server based upon which credentials are being used to make the call. If you logged on to a UNIX machine with different FTP credentials you would see a different directory. To return your own additional unique value you can override the abstract (string) member SessionKey constuctor to return a differentiating string value for an endpoint.
· DefaultEndPointParameters is derived from EndPointParameters. In the default case, its implementation of the SessionKey member returns the OutboundLocation member of the EndPointParameters base class.
· AsyncTransmitterEndpoint is a base class derived from IDisposable. It contains two virtual and two abstract methods to open and reuse an EndpointParameters endpoint. It also enables you to clean up unmanaged resources related to an endpoint's lifetime, and to map the OutboundTransportLocation context property of an outgoing message to an endpoint location. The adapter derives its own class from the AsyncTransmitterBatch class, which uses AsyncTransmitterEndpoint endpoints. This is done when creating a new TransmitResponseBatch batch of messages that the BizTalk Messaging Engine wants the adapter to send on its behalf. As mentioned earlier, the transactional adapter sample does not use the AsyncTransmitterBatch class but rather implements the TransactionalAsyncBatch class directly from IBTTransmitterBatch.
You can use these classes for secondary responsibilities like handling exceptions, loading schemas, manipulating XML, and initialization/termination.
The secondary classes are as follows:
· AdapterManagementBase (AdapterManagementBase.cs). Use this utility helper class for loading schemas to localize resources during design-time adapter configuration.
The design-time user interface for an adapter is implemented by using XSD-based property sheet generation. The text displayed in the property sheet dialog box is extracted from the "displayname" annotations in the XSD file. The adapter writer returns the XSD file as a string to the BizTalk Server Administration console.
The following is a receive location property sheet dialog box followed by a section of code from its associated XSD file, found at <samples path>Adapters Usage\TransactionalAdapter\Admin\TransactionalReceiveLocation.XSD. Notice the "Connection String" property in the dialog box and how it maps to the "Connection String" node in the XSD file with respect to the "The SQL database connection string" text.
<xs:element name="connectionString" type="xs:string" minOccurs="1" default="Data Source='.';Initial Catalog='Northwind';Integrated Security='SSPI';">
<xs:annotation>
<xs:appinfo>
<baf:designer>
<baf:displayname _locID="">Connection String</baf:displayname>
<baf:description _locID="">The SQL database connection string.</baf:description>
</baf:designer>
</xs:appinfo>
</xs:annotation>
</xs:element>
The LocalizeSchemaDOM method is a helper function that helps the adapter writer dynamically alter the XSD schema for the purpose of localization. You can localize XSD schema elements by storing strings to be localized into a standard resource file with the _locID localization attribute. The _locID attribute is purely a convention the code uses to accomplish the localization. Without the convention and without this code it is a tedious process to manipulate all the strings to localize the XSD. The LocalizeSchemaDOM method uses an XPath query to extract all _locID children nodes from that resource file into an XMLNodeList. For each node found, the method finds the localized value from the resource file and dynamically replaces the InnerText value of that node element with the new localized value. When the loop is finished all the localizable elements are inserted into the XMLDocument that is returned. For example, to localize a server's name from English to French use _locID=servername. In the resource file create the value servername="Le Serva". This value is returned and used when the localization ID is French and servername is used in the code.
The following code shows the LocalizeSchemaDOM method:
protected XmlDocument LocalizeSchemaDOM (string schema, ResourceManager resourceManager)
{
XmlDocument document = new XmlDocument();
document.LoadXml(schema);
XmlNodeList nodes = document.SelectNodes("/descendant::*[@_locID]");
foreach (XmlNode node in nodes)
{
string locID = node.Attributes["_locID"].Value;
node.InnerText = resourceManager.GetString(locID);
}
return document;
}
The following code obtains the schema from the resource file as a string. The resource string is passed to LocalizeSchemaDOM, which localizes nodes with the _locID attribute.
string mySchema = GetSchemaFromResource("mySchema");
string myLocalizedSchema = LocalizeSchemaDOM (mySchema, resourceManager);
// where…
protected string GetSchemaFromResource (string name)
{
Assembly assem = this.GetType().Assembly;
Stream stream = assem.GetManifestResourceStream(name);
StreamReader reader = new StreamReader(stream);
string schema = reader.ReadToEnd();
return schema;
}
Note The transactional adapter sample does not use any of the LocalizeSchemaDOM functionality.
· AdapterExceptions (AdapterExceptions.cs). This class derives from ApplicationException, which throws an exception any time a nonfatal exception occurs. It contains numerous utility exception classes that write specific error messages in both the BizTalk Server Tracking database and the Application event log. For example, the class ErrorTransmitUnexpectedClrException is defined in this file as:
public class ErrorTransmitUnexpectedClrException : AdapterException
{
public ErrorTransmitUnexpectedClrException (string message) : base(String.Format("An unexpected failure occurred while processing a message. The text associated with the exception is \"{0}\".", message)) { }
protected ErrorTransmitUnexpectedClrException (SerializationInfo info, StreamingContext context) : base(info, context) { }
}
When an exception occurs within the Worker member function of the AsyncTransmitterBatch class, the exception is caught and an instance of the ErrorTransmitUnexpectedClrException class is created in its private HandleException method, as shown in the following code:
catch (Exception e)
{
HandleException(new ErrorTransmitUnexpectedClrException(e.Message), batch, message);
}
In the HandleException implementation the code calls IBaseMessage.SetErrorInfo(e) to set the error information object for the current thread of execution using the incoming AdapterException object. A call to SetErrorInfo means that the error message appears in the BizTalk Server Tracking database and in the Application event log. In your code you can use these messages as defined in this file or derive your own custom exception class to output the error messages using GetErrorInfo.
· ConfigProperties (ConfigProperties.cs). Use this helper class to extract values from an XML DOM object.
· ControlledTermination (ControlledTermination.cs). This class inherits from IDisposable for resource management of unmanaged resources. Use it when adapter termination is proposed while multiple concurrent threads have outstanding batches still running. The adapter needs to prevent shutdown while the Messaging Engine is processing outstanding message batches. This class provides support to handle pending messages during termination processing. See the "Termination Management" section for more details.
The Base Adapter classes provide support for both receive batches (submitted to BizTalk Server) and send batches (sent from BizTalk Server to an endpoint). These batches can be transactional if the situation dictates this requirement. The following figure shows the class structure surrounding adapter batch processing and the Base Adapter.
The classes in the batch processing hierarchy are as follows:
· Batch. The common base class for all batches used in receiving a BizTalk Server message. This includes the ReceiveBatch, TxnBatch, and TransmitResponseBatch classes, which all inherit directly from Batch. AsyncTransmitterBatch is the only batch class that does not derive from Batch because it does not receive any messages (and thus is not in the preceding diagram). Instead it derives directly from the IBTTransmitterBatch interface because it is a transmitter of BizTalk Server messages.
The Batch class implements the IDisposable and the receiver-specific IBTBatchCallback interfaces. Its function is to interact with the transport proxy and pass in a batch to be processed. All the IBTBatchCallback methods are implemented in this class, and most of them are virtual. You cannot override the BatchComplete method. It calls multiple virtual methods in the Batch class that you may want to override for your custom handling of the batch submission process in your derived class.
· ReceiveBatch. Derives directly from Batch and is used for receiving raw message data, placing that data into BizTalk Server messages, and submitting those messages to the BizTalk Messaging Engine in non-transactional batches. This class contains specific methods to deal with success and failure of message submissions.
Regardless of whether you use transactions in your high-level, derived Base Adapter class, submission of a receive batch always uses an internal transaction. When one message fails submission the entire batch fails as well. Subsequently a new batch must be created for resubmission with the failed ("poison") message(s) removed.
The innerBatch member (of type ReceiveBatch) is used to resolve errors and resubmit a new batch of messages during batch submission processing. It appears throughout the code in the ReceiveBatch.cs module. The innerBatch is the "next-round" innerBatch batch. If the next submission fails, this object recursively continues to remove poison messages and resubmit batches up to n times for a batch size of n messages. Resubmitting n times is the absolute worst case where every submitted message fails. The innerBatchCount member is a check to prevent infinite looping.
· TransmitResponseBatch. Derives directly from Batch and does everything needed for sending a non-transactional batch in an asynchronous fashion. If a failure occurs, TransmitResponseBatch tries to submit the failed message again (Resubmit method). On failure it hands the message to the next transport after the retry count has been exceeded (MoveToNextTransport method), or suspends the message if the backup transport fails (MoveToSuspendQ method).
· TxnBatch. Derives directly from Batch and overrides many base methods for committing or aborting a transaction associated with a message batch. Use this class if you are using a batch in the scope of a transaction. When resubmission of a message fails it is sent to the backup transport. If that also fails the message is sent to the Suspended queue.
· SyncReceiveSubmitBatch. Derives from ReceiveBatch and adds an additional option to wait synchronously on a batch result by using an internal event. This class is useful for invoking code to handle the ordering of messages.
· ReceiveTxnBatch. Contains three classes that all derive from TxnBatch to support transactional receive operations: AbortOnAllFailureReceiveTxnBatch, AbortOnFailureReceiveTxnBatch, and SingleMessageReceiveTxnBatch.
These classes are as follows:
· AbortOnFailureReceiveTxnBatch. This is a transactional batch that aborts if BizTalk Server fails to accept the messages into the MessageBox database. In this case the message data is not persisted to any BizTalk Server database and is returned to the adapter. The adapter still owns the data and is responsible for handling it. A viable option at that point is for the adapter to send the message back to BizTalk Server to place into the Suspended queue. If you are using multiple message batches and transactions, this is the main failure mechanism you will use in almost all cases.
For simplicity, we strongly recommend that you use single-message batches when using transactions. Unless you have pressing performance requirements, always submit a single-message batch through SingleMessageReceiveTxnBatch. For more information, see the "Poison Messages" section.
· AbortOnAllFailureReceiveTxnBatch. Like the previous class, this is a transactional batch that aborts if BizTalk Server fails to accept the messages into the MessageBox database. However, unlike the AbortOnFailureReceiveTxnBatch class, BizTalk Server does not return the raw data to the adapter with AbortOnAllFailureReceiveTxnBatch. Instead it places the messages into the Suspended queue. Do not use this failure method if you want to support strict ordering of messages, because ordering semantics dictate that no messages in the batch end up in the Suspended queue. To support strict ordering, use AbortOnFailureReceiveTxnBatch.
However, if you are writing an adapter for BizTalk Server 2006, you do not need to use the AbortOnFailureReceiveTxnBatch class if you use the new BizTalk ServerActionOnFailure context property. This property is available for any adapter used with BizTalk Server 2006. Set ActionOnFailure to zero in the context property of each messaqe that you do not want BizTalk Server to suspend on a processing exception. Failure to set this property allows BizTalk Server to fall back to its default behavior of suspending the message on a processing exception.
· SingleMessageReceiveTxnBatch. This is a transactional batch that receives a single message. If submission to BizTalk Server fails this class suspends that message within the same transaction. SingleMessageReceiveTxnBatch derives from TxnBatch and resides in the ReceiveTxnBatch.cs file. It receives the CommittableTransaction transaction object in its constructor, which delegates to its TxnBatch base constructor. In the base constructor the CommittableTransaction is used in the call to GetDtcTransaction. This call takes the transaction and promotes it to an MSDTC transaction, returning an IDtcTransaction object instance that represents the distributed transaction. This transaction member takes the passed-in pointer to the CommittableTransaction. Both of these transactional objects are used in the TxnBatch.Done and TxnBatch.EndBatchComplete methods to commit or abort the transaction. Following is the constructor for the base TxnBatch class that performs these actions:
public TxnBatch(IBTTransportProxy transportProxy, ControlledTermination control, CommittableTransaction transaction, ManualResetEvent orderedEvent, bool makeSuccessCall) : base(transportProxy, makeSuccessCall)
{
this.control = control;
this.comTxn = TransactionInterop.GetDtcTransaction(transaction);
// the System.Transactions transaction - must be the original transaction - only that can be used to commit
this.transaction = transaction;
this.orderedEvent = orderedEvent;
}
At a high level the processing flow of messages in the transactional adapter sample occurs as follows:
1. Based upon database configuration values set by the administrator at design time, the receive adapter uses a timer to poll the Northwind database at a set interval and pull data from the Customers table.
2. This data is transformed into a BizTalk Server message and submitted to the MessageBox database using an explicit MSDTC transaction.
3. The send adapter subscribes to the receive port the receive adapter uses and gets the message from BizTalk Server.
4. The send adapter uses a transaction to write the message to a SQL Server table named "scratch."
One of the primary goals of the transactional adapter is to show how easy it is to create and control transactions by using the System.Transactions class. The next sections discuss both sides of this adapter as well as the respective flow of data in and out of the adapter.
One of the last steps in running the transactional adapter sample is to enable the receive location in the BizTalk Server Administration console. If an adapter's host process is running when this occurs, this action generates a call to IBTTransportConfig.AddReceiveEndpoint. It informs the adapter of a new active URI upon which it should listen for incoming messages. For example, invoking AddReceiveEndpoint in the file adapter sample leads to calling the System.IO.DirectorySystem.GetFiles method to get a list of all the files that exist in the directory endpoint the user specified. These files are then opened one at a time and their BizTalk Server message payloads are extracted and then put into a formal IBaseMessage object and passed to BizTalk Server. The transactional adapter sample connects to the Northwind SQL Server database as each timer interval expires and pulls out data to put into an IBaseMessage object to submit to BizTalk Server. This is the type of code you would write in your custom adapter to obtain the message in whatever way makes sense using your protocol and submit an IBaseMessage object to BizTalk Server.
Let's look in more detail at the transactional adapter's implementation of this functionality. The Base Adapter's Receiver class functions as a singleton object broker responsible for lifetime management of endpoints. After it creates the endpoint the Receiver initializes it and adds it to the list of active endpoints maintained by the Receiver. As endpoints are modified or deleted the Receiver class respectively calls Update and Dispose on the endpoint class. In the Receiver class you find the implementations of IBTTransportConfig (called by the Messaging Engine to manage endpoint additions, modifications, and deletions) and IBTTransportControl (used for adapter initialization and termination). In the AddReceiveEndpoint (Receiver.cs) method the Receiver class creates an endpoint whose endpointType is specified as the last parameter in the Receiver constructor. In this architecture there is a TransactionalReceiverEndpoint (derived from the abstract Receiver Base Adapter class) object for each actively configured endpoint, as shown in the following code:
ReceiverEndpoint endpoint = (ReceiverEndpoint)Activator.CreateInstance(this.endpointType);
The endpointType member in this sample is set to TransactionalReceiverEndpoint (last parameter) in the TransactionalReceiver constructor. In the TransactionalReceiverEndpoint class you write your adapter-specific receive operations, such as monitoring a file folder, monitoring an FTP site, or polling a SQL Server database for message data as this adapter does.
The following code shows the TransactionalReceiver constructor:
public class TransactionalReceiver : Receiver
{
public TransactionalReceiver() : base(
"Transactional Adapter",
"1.0",
"Fetch remote messages into BizTalk with a transaction",
"txn",
new Guid("12F8CA70-4DE4-44e8-8F78-D947523F8DEF"),
"http://schemas.microsoft.com/BizTalk/2006/txn-properties",
typeof(TransactionalReceiverEndpoint))
{
}
}
To initialize a TransactionalReceiverEndpoint object, override the Open method and do whatever adapter-specific initializations you need to do before starting the listening process. The Open method sets the handler properties, which in this case pertain to the information needed to poll data from a SQL Server database. Additionally the transport proxy used to interface with the Messaging Engine, the URI where the listening is to occur, and other key properties are stored in member variables.
The following code is the start of the Open method as described above:
this.properties = new TransactionalReceiveProperties(uri);
//Handler properties
XmlDocument handlerConfigDom = ConfigProperties.IfExistsExtractConfigDom(handlerPropertyBag);
if (null != handlerConfigDom)
this.properties.HandlerConfiguration(handlerConfigDom);
// Location properties – possibly override some handler properties
XmlDocument locationConfigDom = ConfigProperties.ExtractConfigDom(config);
this.properties.LocationConfiguration(locationConfigDom);
// This is our handle back to the Messaging Engine.
this.transportProxy = transportProxy;
// Used to control if the BizTalk Server Messaging Engine can unload //us.
this.control = control;
this.uri = uri;
this.transportType = transportType;
this.propertyNamespace = propertyNamespace;
this.messageFactory = this.transportProxy.GetMessageFactory();
Start();
After initialization is complete, Open calls the Start method to activate the receive process. In the transactional adapter it means the polling timer is activated. At design time, the administrator sets the specified unit (U) time interval (T) for polling in the properties for the receive location. As each occurrence of this time interval elapses, the timer's TimerTask method is called.
The following code shows the Start method:
private void Start()
{
this.timer = new Timer(new TimerCallback(TimerTask));
this.timer.Change(0, this.properties.PollingInterval);
}
The TimerTask method simply calls SubmitBatch, where almost all of the adapter's receive processing occurs. In SubmitBatch, the Connection String and Command Text values that were set in the receive location at design time are used to query the Northwind database, extract data, place that data into an IBaseMessage object, and submit it to BizTalk Server in the scope of a transaction. Because this method is large and contains many key points, we will split it up and discuss each point. We will focus primarily on the database operations (Northwind and MessageBox) and the transactions that support them.
There is not much to discuss during the initialization process. Later we discuss termination management in detail, so refer to that section if you feel you need that information at this point to help your understanding. The MemoryStream variable stream is the memory in which the BizTalk Server message will eventually reside when it is submitted to BizTalk Server. Before submission it is used in conjunction with XML DOM objects to structure the data in memory, as shown in the following code:
public void SubmitBatch()
{
bool needToLeave = false;
CommittableTransaction transaction = null;
try
{
// used to block the Terminate from BizTalk
if (!this.control.Enter())
{
needToLeave = false;
return;
}
needToLeave = true;
ManualResetEvent orderedEvent = new ManualResetEvent(false);
string connectionString = this.properties.ConnectionString;
string cmdText = this.properties.CmdText;
string rootElementName = "Root";
MemoryStream stream = new MemoryStream();
bool dataAvailable = false;
Making the interaction with SQL Server transactional is almost trivial thanks to the new Microsoft .NET Framework 2.0 System.Transactions namespace. This powerful class allows code to create and participate in a distributed or local transaction with one or more resource managers. The adapter creates an instance of the CommitableTransaction class to allow the adapter to explicitly commit (Commit method) or abort (Rollback method) a transaction. It calls these methods based upon the outcome of the adapter's transactional code logic, which allows the adapter to uniquely define exactly what transactional success or failure means.
The TransactionScope class is used to mark an entire block of code as transactional. In this block we make the initial read from SQL Server and the injection of the message data into an XML DOM all part of one transaction. This read operation uses the connectionString and cmdText values set in the receive location at design time. The connection.Open method opens the connection to SQL Server by using connectionString. The SqlCommand object takes the cmdText and the connection in its constructor to use when it makes the call to SQL Server to obtain the data by using the ExecuteXMLReader method. The SqlCommand object then writes the data to the stream by using the XMLTextWriter. If no exceptions are thrown during this transactional interaction with SQL Server and XML manipulations, the TransactionScope.Complete method is called. This notifies the transaction manager that all of your operations were successful, the data is consistent and durably stored, and your vote is to commit the transaction. The transaction manager now knows your operations are successful, and can decide to commit or abort the transaction based upon the votes of other leaf nodes and the root of the transaction. Note that committing a transaction does not always mean the message was successfully submitted. It does mean that BizTalk Server has the data, regardless of whether it is placed in the MessageBox database or in the Suspended queue.
In this sample a local transaction is used because only one resource manager—SQL Server—is involved. Because this TransactionScope object created the transaction, the commit action occurs with the call to Complete at the end of the "using" block. In a more complex, real-world, distributed transaction (MSDTC) scenario, other transactional resource managers (TransactionScope objects) also cast their votes with the transaction manager. The transaction manager commits the transaction only if all the resource managers vote to commit it. If any resource manager votes not to commit the transaction, the transaction manager does not commit it. (For more information about these powerful transaction classes, see "Implementing an Explicit Transaction using Committable Transaction" and "Introducing System.Transactions in the .NET Framework 2.0" on ">MSDN).
Continuing with the code in the SubmitBatch method, we see the use of CommitableTransaction and TransactionScope objects. The transaction is passed implicitly to SQL Server by the "using TransactionScope" block. If all goes well the transaction is committed with the call to ts.Complete. It is a good idea to make this the last line of code in the TransactionScope object's "using" block. If you forget to call ts.Complete, MSDTC will abort the transaction even though SQL Server carried out the work correctly.
// Create the System.Transactions transaction
transaction = new CommittableTransaction();
// Explicit interop with COM+ - this should not be necessary in future //(on SQL 2005 for example)
using (TransactionScope ts = new TransactionScope(transaction, TimeSpan.FromHours(1), EnterpriseServicesInteropOption.Full))
{
// The connection is created inside the TransactionScope so the //database will be included in the transaction
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
SqlCommand command = new SqlCommand(cmdText, connection);
// The Encoding on the writer should be consistent with the BizTalk //message. If you change this to UTF8 then you should change the //BizTalk message CharSet to UTF8.
XmlWriterSettings settings = new XmlWriterSettings();
settings.Encoding = Encoding.Unicode;
settings.Indent = true;
settings.OmitXmlDeclaration = false;
using (XmlWriter writer = XmlTextWriter.Create(stream, settings))
{
writer.WriteStartDocument();
// Add the root element because the reader form the database doesn't
// include the root element for the xml - it's just rows
writer.WriteStartElement(rootElementName);
// WriteNode is a little opaque checking the size of the underlying //stream is a simple way to see whether any data was retrieved.
writer.Flush();
long beforeSize = stream.Length;
using (XmlReader reader = command.ExecuteXmlReader())
{
while (!reader.EOF)
{
writer.WriteNode(reader, true);
}
}
writer.Flush();
long afterSize = stream.Length;
// we only want to add a new message to BizTalk if the reader actually //returned anything
dataAvailable = (afterSize > beforeSize);
writer.WriteEndElement();
}
}
// An exception will have skipped this next line
ts.Complete();
}
At this point the message is in memory as an XML object. The comment in the sample code module states, "Typically when polling like this you would want to loop checking dataAvailable and only return to the sleep state when there is no data to feed into BizTalk". This means that if your polling mechanism is similar to this (like the SQL adapter), the code typically loops and reads data as long as there is data to be read (dataAvailable). It returns to a sleep state when there is no longer any data to submit to BizTalk Server.
For simplicity, this procedure is not shown here. If you choose to explicitly put this thread to sleep while waiting for data, be careful to avoid starvation of the .NET thread pool. Sleeping on an increasing number of threads for an extended period of time can cause thread-starvation. This means that there is work waiting for threads but no threads are available. Therefore you have to implement logic in your adapter to ensure there are always threads available to process incoming data as it becomes available. Building in some sort of custom yield in processing, perhaps with a timer, to check for messages during the looping is a good idea. Using BeginInvoke to spin off a different thread to process the work when data becomes visible to the main thread may be an option.
Regardless of the protocol and how you receive the incoming raw message data you probably will read it into a stream. Seek is called to move the pointer to the start of the stream. A SingleMessageReceiveTxnBatch batch object is created to submit the stream (message) to BizTalk Server, as shown in the following code:
if (dataAvailable)
{
// Remember to Seek the stream or we won't get the data
stream.Seek(0, SeekOrigin.Begin);
// Note the batch has been given the CommittableTransaction and it //will take responsibility for Commit
using (Batch batch = new SingleMessageReceiveTxnBatch(this.transportProxy, this.control, transaction, orderedEvent))
{
batch.SubmitMessage(CreateMessage(stream));
batch.Done();
}
orderedEvent.WaitOne();
}
else
{
// No data so no batch but in a database scenario (like this) we might // still wish to Commit. Other transactional adapters might choose to // Rollback in this circumstance.
transaction.Commit();
}
// no exception in Done so we will be getting a BatchComplete which //will do the necessary Leave
needToLeave = false;
}
Error handling in SubmitBatch is straightforward. If an exception occurs, set the ErrorInfo for the event log and abort the transaction by calling CommittableTransaction.Rollback, as shown in the following BatchWorker code:
catch (Exception e)
{
this.transportProxy.SetErrorInfo(e);
if (transaction != null)
transaction.Rollback();
}
finally
{
// if this is true there must have been some exception in or before // Done
if (needToLeave)
this.control.Leave();
}
}
The primary send functionality in this sample is found in the TransactionalAsyncBatch class. This is derived from the IBTTransmitterBatch interface, which provides a batch to the BizTalk Messaging Engine in which to place outgoing messages for the adapter to transmit. The BizTalk Messaging Engine calls TransmitMessage for every message it adds to the batch before calling Done to tell the adapter there are no more messages for transmission.
If IBTTransport.Done completes correctly (does not result in a BizTalk Server exception being thrown), then you have successfully started an asynchronous message submission operation on a BizTalk Messaging Engine thread. In its implementation of this method the send adapter adds each message to an array and returns control to the BizTalk Messaging Engine to add the next message to the batch.
The actual transmission of the messages is done in the BatchWorker method. TransactionalAsyncBatch defines two delegates in the Worker and Batchworker methods. In .NET a delegate is a means to pass a method pointer as a parameter. By being defined as a delegate, an object contains the BeginInvoke method, which allows the delegate (function pointer) to execute on the same main thread as the object. The delegates are defined as follows:
private WorkerDelegate worker;
private BatchWorkerDelegate batchWorker;
In its constructor, TransactionalAsyncBatch instantiates both a worker and a batchWorker object using a WorkerDelegate and a BatchWorkerDelegate respectively. The WorkerDelegate is assigned the method Worker, and the BatchWorkerDelegate the method BatchWorker, as shown in the following code:
this.worker = new WorkerDelegate(Worker);
this.batchWorker = new BatchWorkerDelegate(BatchWorker);
When the BizTalk Messaging Engine has finished adding all its messages to the batch it calls the (IBTTransmitter) Done method in TransactionalAsyncBatch. In its implementation Done calls BeginInvoke on the worker delegate, as shown in the following code:
foreach (DictionaryEntry entry in batches)
{
this.batchWorker.BeginInvoke((string)entry.Key, (ArrayList)entry.Value, null, null);
}
BeginInvoke is an asynchronous call that returns instantly and allows a new thread to execute the Worker method. If you want customized batch processing you can override this method. Worker enters a loop and calls the batchWorker delegate's BeginInvoke method once for each batch of messages to process the batch. This is an asynchronous call into BatchWorker that returns immediately. This permits multiple messages in different batches to be processed concurrently on multiple .NET threads per batch. Each thread is executing in the BatchWorker code, so this method must be thread-safe. Common resources and data that is shared across threads, such as global and static variables, must be protected against concurrent access by using synchronization primitives.
For each message that it transmits, the adapter creates a new message context and transaction of type CommitableTransaction, as shown in the following code:
SystemMessageContext context = new SystemMessageContext(message.Context);
CommittableTransaction transaction = null;
The CommitableTransaction class allows you to create an MSDTC transaction that can be passed to any transactional resource manager to enlist the send operation. For the actual send operation in the transactional adapter the message is written using a transaction to a specific SQL Server table. In the real world an installation could develop a front-end tool to open this table and view the messages for analysis.
Keep in mind that this transactional send to SQL Server is not the same as the transaction used to initially place the newly received message into the BizTalk Server MessageBox database. The send operation used here was an arbitrary choice of the transactional adapter developer. Instead we could have written it in a non-transactional manner to an Excel spreadsheet or called a .NET Web service. Do not confuse the transaction used by the receive adapter to initially submit the message into the BizTalk Server database with this arbitrary transactional operation of outputting the message to a SQL Server table.
An instance of TransactionalDeleteBatch is created whose sole purpose is to abort or commit the transaction. It stores the CommittableTransaction object in a member variable for later use when dictating its outcome. A TransactionScope object is used to make a transactional call to SendMessage to write the message data to SQL Server. If an error occurs, the CommittableTransaction object is used to inform all resource managers associated with this transaction to roll back all their work. A new batch is created and the failed messages that BizTalk Server sent for transmission are submitted back to BizTalk Server to be reprocessed.
The following code shows this processing flow:
private void BatchWorker (string outboundTransportLocation, ArrayList messages)
{
<code removed>
foreach (IBaseMessage message in messages)
{
//Create a new message contxt for each messsage
SystemMessageContext context = new SystemMessageContext(message.Context);
CommittableTransaction transaction = null;
try
{
//Create a commitable transaction to allow to commit or abort the //transaction explicitly. Pass the transaction to the //TransactionalDeleteBatch who will assume responsibility to commit it.
transaction = new CommittableTransaction();
using (Batch batch = new TransactionalDeleteBatch(this.transportProxy, this.control, transaction))
{
using (TransactionScope ts = new TransactionScope(transaction, TimeSpan.FromHours(1), EnterpriseServicesInteropOption.Full))
{
//Write the data to the SQL database.
SendMessage(message, properties);
//Commit the transactional scope.
ts.Complete();
}
//A delete is part of the same transaction of the successful send //operation.
batch.DeleteMessage(message);
//If there was a response to submit a message it would have been added //here using the same batch and same transaction objects as //DeleteMessage.
batch.Done();
}
}
catch (Exception e)
{
// In the scenario we will explicilty Rollback the transaction on //failure.
if (transaction != null)
transaction.Rollback();
//Remember to set the exception on the message itself. This will now //appear in tracking in additio to the event log.
message.SetErrorInfo(e);
//Any failure needs to be retried. But the change of state back on //BizTalk is outside of the transaction that was used to send the //message. After all the transactoin will undoudtly get rollback with a //failure. This batch is non transactional from the adapter's point of //view.
using (TransmitResponseBatch batch = new TransmitResponseBatch(this.transportProxy, new TransmitResponseBatch.AllWorkDoneDelegate(AllWorkDone)))
{
batch.Resubmit(message, false, null);
batch.Done();
}
}
}
needToLeave = false;
}
catch (Exception e)
{
this.transportProxy.SetErrorInfo(e);
}
finally
{
if (needToLeave)
this.control.Leave();
}
}
This section discusses a few additional issues to be aware of when designing and coding your adapter.
Message ordering applies to both the send side and the receive side of an adapter.
On the send side an adapter can process messages in the order in which it receives them from the BizTalk Messaging Engine. However, this is an almost useless fact because the adapter receives many batches concurrently. Multiple adapter worker threads work to transmit batches to the specific endpoint. Send-side message ordering significantly slows down the adapter. This process limits message transmission to one adapter thread, effectively canceling any concurrency.
Message ordering on the send side means the BizTalk Messaging Engine waits for the adapter to call Delete for each message it successfully transfers. After the adapter calls Delete for a message, the Messaging Engine gives the adapter another message to transmit. When you configure ordering on the send port, the BizTalk Message Engine handles the ordering for you.
When configuring a send port there is an Ordered delivery check box. This preserves the outgoing order of messages as it relates to the order in which they were originally submitted into the MessageBox database. This submission could be a result of a receive location or an orchestration initially creating a message. If you bring up the properties for a send port and click Transport Advanced Options you will see the Ordered delivery check box. Selecting it tells BizTalk Server to give a send adapter the next sequential message only after the adapter tells BizTalk Server that the previous message has been successfully sent. After Ordered delivery is selected, you can select the Stop sending subsequent messages on current message failure check box. Enabling this option causes the send port to be suspended when any message fails transmission. Otherwise on message failure only the failed message is suspended and other messages continue to be sent.
Preserving message ordering through this paradigm is costly in terms of throughput performance. One reason for this is the overhead of the internal BizTalk Server sequential message management code that is executed only when this check box is selected. Adapter performance is slowed because the adapter must wait on the completion of the asynchronous Done method. (SyncReceiveSubmitBatch helps you manage this.) To eliminate concurrency, you should run all your message submission code on a single receive adapter thread. The ordering can also be broken if multiple host threads access an adapter concurrently. Therefore, if you want to use ordered message delivery and are using clustering, ensure that your BizTalk Host runs on an active-passive cluster. Normally BizTalk Server is an active-active cluster with multiple hosts.
For performance reasons, it is a good idea for an adapter writer to expose a property to toggle message ordering. Within the adapter's logic it checks to see if ordering is enabled. If so, all batch submission code runs on a single thread as shown in the following pseudocode:
While (messages to process)
{
If control.enter() then return:
Batch b = new SyncReceiveSubmitBatch()
b.Submit (msg1);
b.Submit (msg2);
b.Submit (msg3);
b.Submit (msg4);
b.Done();//this message is asynch
bool bSuccess = batch.Wait()//Will wait on batch complete to reset manual reset event
if (!bSuccess)
{
//Process error but make sure not to break ordering.
//configurable "On failure" dictates what to do here.
}
}
Ordered delivery adds some challenges to error handling. For example, suppose the adapter gets halfway through a batch and a message fails in its submission process. (For example, in the preceding code, bSuccess returns a failure code from batch.Wait. What do we do if we want to preserve ordering? In some adapters a failure action is configurable and that choice is left up to the administrator. For example, for a receive location configured to use the MSMQ adapter there is the On Failure property (found under Properties/Configure). If the administrator sets the Ordered Processing property to true, the On Failure property becomes enabled and offers three options:
· Stop prevents the adapter from receiving any more messages.
· Suspend (Non-resumable) moves the message into the Suspended queue and marks it as not resumable.
· Suspend (Resumable) moves the message into the Suspended queue and marks it as resumable.
The requirement for receive-side ordering is that you must wait for the previous batch to complete before submitting a new receive batch. This is done by the Messaging Engine calling back into the adapter's batchComplete method.
A problem could occur in the ordering if the BizTalk Server default error processing is allowed to execute. Normally BizTalk Server suspends messages and returns a warning instead of an error code to the adapter. If BizTalk Server chooses not to handle the bad message it returns an error to the adapter. The MSMQ adapter's ActionOnFailure property can be set from within the adapter's receive location configuration. The adapter developer should provide a user interface similar to the one for the send port. In the receive adapter's error-handling block (like the "if (!bSuccess)" block above), the code examines this property's configured value. Depending upon its setting BizTalk Server takes appropriate action when a message fails submission. For example, if the MSMQ adapter sees this value set to 0 it enables logic to tell BizTalk Server not to suspend a message on a submission failure. This allows the adapter to implement its own handling to dictate its own policy on message ordering.
Database adapters also need to consider ordering, locking, concurrency, and serialization. Serialization makes it easier for the stored procedure writer to avoid deadlock and performance drains due to concurrent locking, yet offers the worst throughput. A high level of concurrency against a database does not necessarily result in higher throughput due to potential table and record locking. The earlier discussion about message ordering on the send side basically came down to stopping the concurrency. A database adapter developer might use similar techniques to reduce the concurrency.
Typically a database adapter runs queries at set intervals using transactions against the database to pull data into BizTalk Server messages. After data is successfully transferred to BizTalk Server the adapter deletes each message (row). In isolation this does not cause concurrency problems.
Production database adapters are typically written to check if there is more data to read from the database and put into a BizTalk Server message. These adapters poll for data. When the adapter finds data, it keeps looping as long as there is more data, and submits the data to BizTalk Server. If it finds no data the adapter typically goes to sleep for a small fixed time interval. When the interval expires, the adapter wakes up to see if there is any new data.
The following pseudocode shows this process:
do
{
do
{
//loop polling for data
} while (more data)
Sleep (a few seconds)
} while (adapter enabled)
One potential pitfall in this approach is that this inner loop continuously reads data while it is available. This can result in starving the .NET thread pool because your loop is probably executing on a thread-pool thread (for example, from a timer), and does not release the thread until there is no more data. This could starve other receive locations. It might be better to execute a single batch of messages and then reschedule your operation on the .NET thread pool. This way is more fair to other send operations (potentially other receive locations) sharing the same .NET thread pool.
The following are the main points related to message ordering:
· Errors and how they are handled can corrupt the order of messages. The adapter writer should provide a configuration option or default error-handling processing to ensure that an error does not corrupt the entire operation. In the worst case, the adapter might simply shut itself down to avoid corruption due to a break in the ordering.
· The receive side should be single-threaded and truly synchronous in its message processing. However, the receive adapter submits messages to the BizTalk Messaging Engine in an asynchronous manner. Normally this is not an issue, but asynchronous submission does not work with respect to message ordering. It is even more complicated in that additional asynchronous batches (innerBatch) may be submitted during error processing.
The solution is to make the batch processing more synchronous in nature. The Base Adapter classes do this in three ways. The first technique uses the callback, the second uses an event parameter, and the third creates and uses an event that enables you to wait on the batch after you have called Done. The transactional adapter is inconsistent in how this is done and could lead to confusion.
Any message in a batch that fails when submitted to BizTalk Server can be called a "poison" message. In a sense its failure "poisons" the batch of messages because it prevents the other messages from being submitted as long as it remains part of that batch. The adapter code must implement logic to deal with this type of message. In a single-message batch the adapter can suspend the poison message and fail submission of the batch under the same transaction. In a multi-message batch the entire batch must be aborted even if only one of its messages is poison.
For an example of a poison message, assume that there are fifty messages to be submitted. The adapter divides them into five batches of ten messages each. Each batch is submitted immediately after the previous one has been successfully submitted. (Submission here means handing the batch to a BizTalk Server worker thread for processing.) The BizTalk Server worker threads process each message by entering it into the MessageBox database as part of one transactional operation. If any message fails the submission process, the entire batch submission fails and the transaction is rolled back. Because the Done call that submits the message batch to the worker thread is an asynchronous operation, all five batches will most likely be processing concurrently at some time.
Suppose batches 1, 3, and 5 were processed to completion and their transactions committed them into the MessageBox database. Unfortunately, batches 2 and 4 contained poison messages and failed submission, yielding an error. The adapter asynchronously aborts those transactions and the messages from batches 2 and 4 reappear in the adapter's private work queue. If you are using the adapter's batch classes you do not need to implement BatchComplete to handle this situation. The retry algorithm shifts back into single-message batch mode processing until the poison messages have been submitted successfully to BizTalk Server. (Recall that successful submission means being published into the MessageBox database or placed into the Suspended queue.) At that point the retry algorithm shifts back to multi-message processing for the sake of performance. An additional explanation of this scenario is in "Writing Effective BizTalk Server Adapters" (http://go.microsoft.com/fwlink/?LinkId=65211)
under "Transactional BatchComplete Errors".
SingleMessageReceiveTxnBatch submits a batch with a single message. If you do not care about performance this is the best batch class to use. However, if performance is important, then use the multi-message scheme with either AbortOnFailureReceiveTxnBatch or AbortOnFailureAllReceiveTxnBatch. Use this scheme until you get an error, and then switch to the single-message scheme and submit messages individually until you locate and suspend the poison message. Then you can return to the multi-message paradigm if you want to. If you use SingleMessageReceiveTxnBatch, the base class does all the mode-swapping work for you.
To summarize, use AbortOnFailureReceiveTxnBatch and SingleMessageReceiveTxnBatch together to asynchronously process message streams with efficiency while handling poison messages. Both classes implement BatchComplete and implement proper error handling. If you do not care about maximum throughput optimization, then submit single-message batches with SingleMessageReceiveTxnBatch. Because this submits single messages, it effectively disables batching and thus is slow.
The following pseudocode shows how to use multi-message mode:
Commitable Txm //not a dtc tx but you promote it on demand in system.transactions
DtcTxn = Txn.GetDTCTransaction //promotes since BTS needs a DTC tx
Batch b = new AbortOnFailureTxnBatch(DtcTxn)
Batch.submit (msg1);
Batch.submit (msg2);
Batch.submit (msg3);Batch.Done()
The following code shows how to transmit single messages in a batch:
//not a dtc tx but you promote it on demand in system.transactions Commitable Txn
//promotes since BTS needs a DTC tx)
DtcTxn = Txn.GetDTCTransaction
Batch b = new SingleMessageReceiveTxnBatch(DtcTxn)
Batch.submit (msg);
Batch.Done()
None of the Base Adapter's Batch objects used for receive operations are thread-safe. This means that if you use the same Batch object from multiple threads (sharing an object's reference), then any operations on the batch should be serialized. Otherwise, data corruption can occur. The following pseudocode illustrates this point:
lock(batchObject)
{
batchObject.Submit(msg);
}
The non-transactional ReceiveBatch batch class continues to submit multiple batches until it is successful. For example, suppose you have a ReceiveBatch with ten messages and the fourth message fails submission. ReceiveBatch creates a second batch and includes the first poison message destined for the Suspended queue (it calls IBTTransportBatch.MoveToSuspendQ on that message instead of IBTTransportBatch.SubmitMessage). If a failure occurs in the next batch submission, it continues to call Submit for each remaining good message and MoveToSuspendQ for each bad message in the batch. Each batch is aborted until it finally has all the poison messages as calls to MoveToSuspendQ and all the good messages as calls to Submit in one final batch, which is the only batch that finally commits.
BizTalk Server invokes an in-process adapter's Terminate method when the BizTalk Server service is shutting down. BizTalk Server termination processing dictates that an adapter must block processing in its Terminate method until all pending messages in all its submitted batches have processed to completion. This includes messages either in a batch submitted to the BizTalk Messaging Engine or waiting on a message response from a client. It is the adapter's responsibility to keep track of every batch thread it has executing. The BizTalk Messaging Engine calls back into the adapter's IBTBatchComplete.BatchCallback method once for each batch processed. After the last callback all its batches have been processed to completion and the adapter can return from Terminate.
As you can imagine, the adapter synchronization code associated with this process in a multithreaded environment can be very complex. The Base Adapter does a good job of managing this for you with the ControlledTermination class. You only need to understand how to harness this capability.
The ControlledTermination class acts like a standard reader-writer lock. That type of synchronization-lock primitive has multiple readers but only one thread has exclusive write access. The reader-writer lock implements this functionality for you. Your responsibility is to make sure you properly acquire a lock to write data and release it promptly when you are finished with it.
There is only one global ControlledTermination object per adapter. The constructor for every Base Adapter batch class takes a controlled termination object. Make sure you pass a ControlledTermination variable to whatever batch object you are using. The following code inside the SubmitBatch method (TransactionalReceiveEndpoint.cs) shows the constructor call to create a SingleMessageReceiveTxnBatch batch. It takes the ControlledTermination object as the control parameter.
using (Batch batch = new SingleMessageReceiveTxnBatch(this.transportProxy, this.control, transaction, orderedEvent))
When using the ControlledTermination class your adapter should explicitly call the Enter method every time it enters a thread. This includes a thread waiting on a callback from BizTalk Server. Correspondingly, your adapter should explicitly call Leave every time it exits a thread. The Batch class transparently calls Leave for you automatically if Done was called without any errors.
ControlledTermination keeps an activity count using a custom synchronization primitive that coordinates multiple activities. An activity represents a submitted batch of messages and is tracked by using the module-level global variable activityCount. This value is incremented each time Enter is called and decremented when Leave is called. If the Terminate method has been called but the adapter has not shut down, the terminate member will be true. That prevents any new threads from starting up when they call Enter. Instead it blocks until all threads currently executing are finished and the activityCount falls to zero.
The following code shows the Enter and Terminate methods and their use of the activityCount variable to make sure termination does not occur with any outstanding submitted batches.
// to be called at the start of the activity
// returns false if terminate has been called
public bool Enter ()
{
lock (this)
{
if (true == this.terminate)
{
return false;
}
this.activityCount++;
}
return true;
}
// to be called at the end of the activity
public void Leave ()
{
lock (this)
{
this.activityCount--;
// Set the event only if Terminate() is called
if (this.activityCount == 0 && this.terminate)
this.e.Set();
}
}
// this method blocks waiting for any activity to complete
public void Terminate ()
{
bool result;
lock (this)
{
this.terminate = true;
result = (this.activityCount == 0);
}
// If activity count was not zero, wait for pending activities
if (!result)
{
this.e.WaitOne();
}
}
The needToLeave variable is used throughout the batch classes to prevent Terminate from continuing improperly with outstanding batches that have not been completely processed. If the value of needToLeave is true, then an error occurred in the processing of Done. This means that an outstanding Enter was called and Leave will not be called due to errors. You will have to handle the errors and call Leave manually, typically in the "finally" block. If the Done call on the batch succeeded, then needToLeave is false and the Base Adapter class calls Leave for you.
The following code from the ReceiveBatch class shows both the needToLeave and innerBatch (discussed earlier in the "Batch Processing Class Hierarchy" section) members:
protected override void EndProcessFailures ()
{
if (this.innerBatch != null && this.innerBatchCount > 0)
{
try
{
this.innerBatch.Done(null);
this.needToLeave = false;
}
catch (Exception e)
{
Trace.WriteLine("ReceiveBatch.EndProcessFailures Exception: {0}", e.Message);
this.innerBatch = null;
}
}
}
protected override void EndBatchComplete ()
{
if (this.needToLeave)
this.control.Leave();
// if there is no pending work and we have been given an event to set // then set it!
if (this.innerBatch == null)
{
// Theoretically, suspend should never fail unless DB is down/not- //reachable or the stream is not seekable. In such cases, there is a //chance of duplicates but that's safer than deleting messages that are not in the DB.
if (this.ReceiveBatchComplete != null) this.ReceiveBatchComplete(this.OverallSuccess && !this.suspendFailed);
if (this.orderedEvent != null)
this.orderedEvent.Set();
}
}
The previous sections discuss how the Base Adapter classes are structured as well as the roles they play in the transactional adapter sample. This gives a theoretical understanding of the power of these classes, but does not show how to make the best use of them. In this section we look at how you apply the classes to your adapter solution in clearly defined steps.
Regardless of whether your adapter is send or receive, or both, you create a new Class Library project for the run-time functionality. Add references to the following assemblies:
· Microsoft.BizTalk.Adapter.Framework
· Microsoft.BizTalk.Pipeline
· Microsoft.Samples.BizTalk.Adapter.Common
· Microsoft.BizTalk.GlobalPropertySchemas
· Microsoft.BizTalk.Interop.TransportProxy
· Microsoft.XLANGs.BaseTypes
For adapter design-time management you create another Class Library project, and in that project you only need references to the first three assemblies listed above. You also need to create two XSD files for send (transmithandler.xsd and transmitlocation.xsd) and two for receive (receivehandler.xsd and receivelocation.xsd) design-time configuration.
On the receive side you use the Receiver, ReceiverEndpoint, and one of the derived Batch classes. Perform the following steps:
1. Subclass the Receiver class (refer to the TransactionalReceiver class). In the constructor, add the transport type, GUID, property namespace, and your receive endpoint. This acts as a class factory for the ReceiverEndpoint class.
2. Subclass the ReceiverEndpoint class to create your own receiver type (refer to the TransactionalReceiverEndpoint class). This class listens or polls the external system, and when data is available it retrieves that data (optionally within a transaction), creates a BizTalk Server message, and creates a batch class.
3. Select a batch class depending upon whether you are using transactions or more than one message in a batch.
The AsyncTransmitterBatch class does all the work for a send adapter. Recall that the transactional sample does not use this class. You can view the use of this class from within the file adapter sample in its DotNetFileAsyncTransmitterBatch class.
A non-transactional send adapter is rather straightforward. Perform the following steps:
1. Subclass the AsyncTransmitter class (see the TransactionalTransmitter class in the sample) to send a batch to BizTalk Server when it needs to send messages. The constructor is the same as the receive side where you add the transport type, GUID, property namespace, and transmit endpoint.
2. Subclass the TransmitterEndpoint class and add code to send the message. Again the transactional adapter sample does not use this class. Refer to the DotNetFileTransmitterEndpoint class in the file adapter sample for an example of how to implement this functionality.
A transactional send adapter is a bit more complex. Perform the following steps:
1. Just like a non-transactional adapter, subclass the AsyncTransmitter class.
2. Additionally in this class you override the CreateAsyncTransmitterBatch method to return a new instance of an AsyncTransmitterBatch object (refer to the TransactionalTransmitter class). For this batch object you can either subclass from AsyncTransmitterBatch (DotNetFileAsyncTransmitterBatch class in the file adapter sample) or implement IBTTransmitterBatch directly (TransactionalAsyncBatch class in the transactional adapter).
After your adapter is compiled it needs to be installed into the registry on the host computer upon which it will execute. For example, the registry needs to know the path to your assembly, its alias to display in the properties dialog boxes, the GUIDs of the send and receive adapters, and so on. You can manually create this file but it will be prone to errors due to all the nested folders and GUIDs. We recommend that you use the Adapter Registration Wizard that ships with the SDK in the <installation directory>\SDK\Utilities\AdapterRegistryWizard folder. To create the registration file refer to the "Adapter Registration Wizard" topic on ">MSDN at http://go.microsoft.com/fwlink/?LinkId=64298. This wizard outputs a .reg file that you can import into the registry. This file is described in detail at http://go.microsoft.com/fwlink/?LinkId=64299. You can view an example of its output at by looking at the TransactionalAdmin.reg file located at <samples>\AdaptersDevelopment\TransactionalAdapter\Admin.
Earlier we examined the high-level class derivations and instantiations that need to occur for receive and send adapters. Now we look deeper into the interface implementations and semantics of sending and receiving messages. This encompasses working both with and without transactions.
When sending a message the transaction is established with the target resource manager. In the transactional adapter sample that resource manager is SQL Server (refer to the BatchWorker and SendMessage methods in TransactionalAsyncBatch). A transactional send operation proceeds as follows:
1. BizTalk Server sends to the adapter a list of messages to be transmitted in the batch the adapter provides. With each call on the batch's IBTTransmitterBatch.TransmitMessage method, build a list of messages one at a time. After IBTTransmitterBatch.Done is called, sort the list by endpoint, create a transaction, and send the messages to the external system (refer to the TransactionalAsyncBatch class).
2. The adapter creates a transaction TXN1 and submits the messages to the external transactional system (for example, Oracle or SQL Server) using TXN1.
a. If the transmission succeeds, the adapter creates a batch of message deletions and submits them back to BizTalk Server to delete the batch using the same transaction (TXN1). The adapter then commits the transaction explicitly from its code.
b. If the transmission fails, the adapter aborts the transaction. It then creates a new, non-transactional batch and resubmits the message to BizTalk Server. If Resubmit fails, you need to handle the message. Typically you do this by calling MoveToNextTransport or MoveToSuspendQ.
Message deletions (following a successful one-way send) or sending a response message (second part of a solicit-response operation) can never be part of the same transactional batch that was used for Resubmit, MoveToNextTransport, and MoveToSuspendQ.
1. BizTalk Server sends to the adapter a list of messages to be transmitted in the batch the adapter provides. The adapter may sort the messages by endpoint.
2. The adapter submits the messages to the external system.
a. If the transmission succeeds, the adapter submits the message back to BizTalk Server to delete it using a batch.
b. If the transmission fails, the adapter resubmits the message to BizTalk Server. If using multiple messages, the batch used here is usually part of the same batch used to transmit in the previous step.
1. Create transaction TXN1 in the receive adapter.
2. Obtain the message from the external system using transaction TXN1.
3. Create a transactional batch. If using a transactional batch use SingleMessageReceiveTxnbatch for single-message batches, or AbortOnFailureReceiveTxnBatch, which handles multiple messages. With the latter you must write code to abort on error, go to single-message mode, and eventually go back to batch mode.
4. Submit the message to BizTalk Server using the batch class and TXN1. Call Done when the batch is ready to go and loop as you see fit for more data. Realize that different systems have different ways of peeking or waiting on data so you will have to do what is appropriate for your protocol. For example, POP3 has a login delay, and the MSMQ adapter might run out of sockets.
a. If the transmission to BizTalk Server succeeds, then commit the transaction TXN1.
b. If the transmission to BizTalk Server fails, then:
If using a multiple-message batch AbortOnFailureReceiveTxnBatch, then abort the transaction TXN1.
If using a single-message batch using the SingleMessageReceiveTxnBatch class, then call MoveToSuspendQ, and commit the transaction TXN1.
1. Obtain the message from the external system using the receive adapter.
2. Create a non-transactional batch. For a non-transactional batch use ReceiveBatch and SyncReceiveSubmitBatch if doing message ordering.
3. Submit the message to BizTalk Server using a non-transactional batch.
a. If the transmission to BizTalk Server fails, then call MoveToSuspendQ to suspend the failed message.
In this section, we debug an adapter. This process will teach you about the transactional adapter by walking through the code. It will also show you how to debug your own adapter during your development process.
To debug the transactional adapter, follow these steps:
1. Configure the transactional adapter with the instructions in BizTalk Server 2006 Help under Development\Samples in the SDK\Adapter Samples – Development\Transactional Adapter.
Ensure that the Northwind database exists on your system by using the Microsoft SQL Server Management Console. If necessary, install it from http://go.microsoft.com/fwlink/?LinkId=64296. You must do this before running the SQL query script in "Running the Sample" to install the stored procedure and create the table in the Northwind database.
When you get to the end of the "Running the Sample" section, just for now, do not enable the receive location or start the send port. We need to set breakpoints and we want to track what the send adapter is doing to SQL Server.
2. Open the BizTalk Server Administration console and stop the BizTalkServerApplication host if it is started.
3. Open Visual Studio 2005 and load the transactional adapter's solution file (transactionaladapter.sln).
4. Open the file TransactionalReceiverEndpoint.cs and find the SubmitBatch method. This is where the primary receive processing occurs. Move your cursor to that line and press F9 to add a breakpoint.
5. Open the file TransactionalAsyncBatch.cs and find the Batchworker and Send methods. This is where the primary send processing occurs. Move your cursor to these lines and press F9 to add a breakpoint.
Note that due to the asynchronous nature of the adapter, the breakpoint execution flow may not work as you are accustomed to. Therefore feel free at this point to set breakpoints on other key lines of code you have an interest in watching execute. Be aware that you may need to disable the breakpoints on the send side to view the receive side, and vice versa. Also try to set breakpoints past any of the termination and synchronization blocks that have to do with the Enter method and the needToLeave variable. Breakpoints set there can cause an endless loop in some situations due to timing.
6. Open Microsoft SQL Server Management Studio to monitor the "scratch" table in the Northwind database. From step 1 above the Northwind database should already be configured correctly with a table named "scratch."
7. Enable the receive location TxnReceiveLocation1 and start the send port TxnSendPort1. Make sure to "Start" the send port and not just "Enlist" it. ("Start" both begins its processing and enlists it for you in one operation.)
8. Start the BizTalkServerApplication host.
9. In Visual Studio, on the Debug menu, click Attach to Process, select Show processes from all users, select BTSNTSvc.exe, and click Attach.
10. As soon as the configured time interval for the receive location is triggered breakpoints will be hit and control will jump to Visual Studio. Use F10 to step over functions and F11 to step into them.
11. In Microsoft SQL Server Management Studio, select New Query, enter the following, then press ENTER:
Use Northwind
Select * from Scratch
This shows you each row that is being written to the "scratch" database table. To remove all rows and see it start over, replace the select statement with "Delete Scratch".
The Base Adapter classes are one way to develop adapters. With the 2006 release of BizTalk Server they have been updated to provide transactional support for both send and receive adapters. Understanding the methods you override or implement and what functionality the Base Adapter provides for free are keys to a productive adapter development cycle. Using the Base Adapter is a great way to write adapters after you understand the execution path and which code (yours or the Base Adapter's) is responsible for what processing.
The following points summarize the main areas to implement when writing your adapter:
· In a receive adapter, inherit from the Receiver class. Create a ReceiverEndpoint for every receive location and write your transport-specific code to receive a BizTalk Server message. Choose and instantiate a class derived from Batch to submit the messages one at a time or in a batch to BizTalk Server. Use transactional classes as applicable.
· In a non-transactional send adapter, inherit from the AsyncTransmitter class. Create a TransmitterEndpoint and add code to send the message.
In a transactional send adapter, the steps are similar but use AsyncTransmitterBatch or IBTTransmitterBatch to pass to the BizTalk Messaging Engine and get its messages to be sent. Sift through these messages per endpoint and transmit them.