Loosely Coupled Transactor Server


Loosely Coupled Transactor Server

Intent

Provide an asynchronous server-based solution primarily for clients that do not have .NET capabilities and for clients that do not wish to utilize the built-in asynchronous features of .NET and of the Web services proxy-generator tools that come with the framework.

Asynchronous System Design Challenges

Many of you may have already had exposure to asynchronous-based development. If you have ever worked with Microsoft Message Queue Server, TIBCO, or any other asynchronous platform/API, you may already understand the benefits as well as the challenges that coincide with designing such a system. If you have worked with any of these or similar frameworks, you probably have realized that although they may provide the "async" plumbing, the design is still up to you. In fact, it is putting together a sound async design that is the most difficult. The technology behind an async model provides you only with the means of saving state, coordinating action, and possibly notifying the user during state changes. It is this plumbing that your model is designed on top of but it is the overall structure of the objects that are themselves independent of any technology. It seems as though developers initially think that as long as they have TIBCO or MSMQ, the async model will take care of itself. This is contrary to reality, and it is only when complicated async processes are implemented that those issues become plainly apparent. Some typical problems seen in async systems including the following:

  • Users do not have TIBCO or another async API installed so that they can receive notifications.

  • The business operation returns a very large DataSet.

  • An exception occurs after the caller is disconnected.

  • A two-phase commit operation must span across multiple async operations.

  • Queued business operations continually fail and become stale while remaining in the queue (for those async systems using durable queues).

Many systems have the luxury of being capable of correct design from inception. These systems can take into account the logic necessary to facilitate an async system and are thus better positioned to answer some of these issues above. Typically, however, it seems more common that existing synchronous (sync) systems are those that are targeted to receive async features. Usually, this is a result of poor performance from an existing application and the need to improve scalability. This is indeed more of a challenge. Now you must take existing business logic and retrofit it into an async environment. Most "blocking" or sync methods that must be designed to be async usually must first go through a redesign effort. Usually, you cannot simply put an async wrapper around a sync operation and expect everything to run as before. Not only do such systems undergo a form of technical redesign but they may also undergo a business flow redesign. The amount of refactoring depends on the order of events that must take place within the method logic and what dependencies it may have during execution. Determining these dependencies will help flush out those elements that require attention.

When designing an async function, I suggest first answering some of the following questions:

  • What data will be returned to the caller, if any?

  • Is the method involved in any distributed transactions?

  • Will there be compensating logic required in case of failure (e.g., undo functionality)?

  • What exception scenarios will affect the success or failure of the logic?

  • How do you communicate to a disconnected caller?

  • How many concurrent users must you communicate with or notify?

  • Where can you control state? On the server? On the client?

  • What form of state management will you use?

  • Can transactions go stale? What timeouts, if any, should be employed?

  • If using any form of durable queues, what cleanup and/or administration tasks will be required of the system?

  • Will storing large result sets in a job table defeat the purpose of calling an operation asynchronously (therefore, will the overall latency between calling and receiving be too lengthy)?

The questions listed above mainly deal with three primary focus areas: state management, caller communication, and disconnected coordination/administration . State management simply deals with making information durable, whether that be saving data to a database or a file, or using a shared in-memory object. The goal of any asynchronous system is to release callers as soon as possible once the method is invoked so that they may go about their business. Doing so typically involves capturing enough information and continuing the processing of the transaction without blocking the caller. Once the transaction is complete, the response must be made available to the user, either by direct notification or by saving the response. Another use of state management is guaranteeing that what the caller has invoked will indeed eventually run to completion at some point, providing a transaction "status facility." This is typically where message queuing becomes useful by providing a responsive environment for users where once they fire off transactions, they can go about their work and still have the ability to see whether transactions are processing or have completed.

The second area that must be thought out and designed is that of caller communication. This relates to how information will be presented to the user. The user can either be notified of server events directly or may poll the server, seeking status of a previously invoked method. Each has its advantages and disadvantages. Suffice it to say that employing notification is a convenient and efficient means of communication but is very technology-dependent and may not be a scalable option for your application (e.g., Internet applications). Polling is less efficient in that multiple calls may have to be made to assess the state of an async operation but it is beneficial in its simplicity and maintainability. You may also choose to provide notification for simple status messages and polling for large DataSets. This is something I have found to be quite successful as long as a notification and broadcast technology was deployed (e.g., TIBCO). Using MSMQ for broadcasts (one-to-many notifications) will be practical only in smaller environments. For larger environments, I recommend using a third-party message broadcast facility, rolling your own, or using a messaging server, such as Exchange.

The third focus area in asynchronous design is that of disconnected coordination and administration. This relates to the first two in that typically, state must be held to help coordinate a multistep transaction. Also each step of the transaction may require user notification and interaction, depending on how complicated the operation may be. Async models become complicated when many steps require an ordered, coordinated action. You also have to address the freshness of the transaction being invoked. If failure occurs ”and it always does ”will the transaction be placed back into a queue? If it is, how long should it remain in the queue? Using a message queue system will aid in answering these quandaries through implementation but they are no substitute for a plan that is well thought out. This focus area will probably be the one that requires the most attention. It will also be the one that requires the most administration. For example, when utilizing a message queue environment for transactional multistep processes, there will be instances when transactions never complete. Unless there are administration processes and tools in place, a queue can quickly become overtaken by stale messages. This is where the K.I.S.S. (keep it simple stupid) principle really needs to be applied. By keeping your async model simple, you will ease your coordination pains in the future.

I hope this won't scare you off from implementing async systems but it should serve as a slight warning. These systems shine in their ability to improve the scalability of most applications immediately and, to the delight of the user, no longer block users from performing other tasks while a transaction is running in the background. However, they can also become quite complicated and warrant significant design attention, especially when operations employ multiple steps or return large DataSets. The LCT Server presents a server-based asynchronous model that employs polling for those clients that do not have the technology for direct callbacks and/or notifications. It also provide a rather simple means of wrapping an existing synchronous system in an asynchronous cover that minimizes those async refactoring challenges mentioned previously.

Problem

The reason for even considering going async is simple ”the need for speed and scalability. There is also the added benefit of freeing up the user to do his or her work while a transaction may be running in the background. Not many of us like to see an hourglass while running a lengthy operation when we could be reading email. But for designers and system administrators, the real benefit is improved performance and scalability. With async invocations, the server is free from maintaining long-running connections with what could be hundreds of clients. This frees up memory and other valuable resources, which in turn improves scalability. With freed resources you now have improved performance. This is especially true for Web services applications. In fact, I highly recommend leveraging async processing whenever possible to improve your Web service performance overall.

Much thought was placed in the area of async features as they relate to the .NET BCL, as well as the tools accompanying VS.NET. Instead of an afterthought as it is with some languages, .NET has built into the framework an asynchronous programming model. This programming model is seen in the networking features, such as .NET remoting, sockets, and as you'll see here, XML Web services. In fact, it couldn't be any easier to implement. Simply by instantiating the remote object (or more accurately, its proxy) and calling a remote method, you unblock the caller through async invocation. Microsoft has even taken the asynchronous model a step forward by standardizing the remote method names . For example, if an operation is called Foo() , then the remote async version of that will be called BeginFoo() and EndFoo() . With the aid of proxy generators such as WSDL.exe, you do not need to create specific async versions of methods or their signatures. The WSDL.exe tool or VS.NET will generate the async versions for you from the method signature or, more specifically , the WSDL output. Listing 7.12 shows both the Execute method sample from a Chained Service Factory (Chapter 4) and the sample wsdl.exe output generated from VS.NET.

Listing 7.12 is a code snippet from the Chained Service Factory Web method called Execute .

Listing 7.12 Code snippet from a Web Service Factory used by a typical LCT Server.
 public class ChainedServiceFactory : System.Web.Services.WebService    {       . . .       [WebMethod]       public DataSet Execute(DataSet dsData, string sService)       {          DataSet ds = new DataSet();          Facade oFacade = null;          try          {             switch (sService)             {                case "Transactor":                   oFacade = (Facade) new          TransactorFacade();                   break;                case "Report":          oFacade = (Facade) new          ReportFacade();                   break;                default:                   return ds;             }          }          catch (Exception e)          {          throw new BaseException(. . .);          }          // invoke the factory method on the facade          ds = oFacade.Execute(dsData);          return ds;       }    } 

Next , the generated proxy code from VS.NET (the Begin and End methods are automatically generated for you):

Listing 7.13 WSDL-generated output for the code snippet in Listing 7.12.
 public class ChainedServiceFactory :    System.Web.Services.Protocols.SoapHttpClientProtocol {    /// <remarks/>    public ChainedServiceFactory()       {          . . .       }    /// <remarks/>    [System.Web.Services.Protocols.SoapDocumentMethodAttribute(. . .)]       public System.Data.DataSet Execute(       System.Data.DataSet dsData,       string sService)          {             object[] results = this.Invoke("Execute", new object[] {                dsData,                sService});             return ((System.Data.DataSet)(results[0]));       }       /// <remarks/>       public System.IAsyncResult BeginExecute(       System.Data.DataSet dsData,    vstring sService,       System.AsyncCallback callback,       object asyncState)          {             return this.BeginInvoke("Execute", new object[] {                   dsData,                   sService}, callback, asyncState);          }       /// <remarks/>       public System.Data.DataSet EndExecute(       System.IAsyncResult asyncResult)          {          object[] results = this.EndInvoke(asyncResult);          return ((System.Data.DataSet)(results[0]));       } 

The same goes for other asynchronous features of the .NET BCL. To create a socket client, for instance, you call BeginConnect() instead of Connect() to make a remote socket connection. You can even build an asynchronous socket server using its built-in async methods. In fact, just by using Intellisense from within VS.NET, you can immediately see which class contains async support by looking for methods beginning with Begin or End . Whether an operation resides as part of a Web service, a socket server, or some other object residing in another app domain on the same machine, .NET gives you the power to call it asynchronously.

Now that your mouth is watering with all of the great asynchronous features of .NET, there will be disappointments. Not every client implementation has the luxury of running .NET. Also there may be instances where changing client code becomes extremely prohibitive. For those cases, the LCT Server may aid in still providing an application with sorely desired async features. The LCT Server provides a server-side async wrapper around existing synchronous applications, and it does so rather unobtrusively. As stated earlier, it is difficult enough to design an async model, but it is considerably more difficult when having to retrofit an existing sync environment to provide async behavior. Does this mean that the LCT Server would never be used for a .NET client to .NET server application written from the ground up? No. In fact, due to its simplicity, it can be a good first design choice for providing async features with minimal headache for your users, as you will see.

Forces

Use the LCT Server pattern when:

  • Adding asynchronous services to an existing synchronous system.

  • Adding client notification is neither practical nor possible.

  • Asynchronously returning large result sets through polling.

  • Clients do not have .NET installed or you do not wish to add asynchronous logic at the client level.

  • You want to be introduced to asynchronous logic slowly, when a simple introduction to the technology is required.

Structure

The structure begins when any client (does not have to be .NET) wishes to execute a synchronous operation a synchronously. The client first instantiates the TransactorService object instead of connecting to the original object. The client then calls Execute() on the TransactorService object, passing in any parameters required to send to the original synchronous operation (Figure 7.5). I highly recommend bundling parameters in a flexible, self-describing container, such as a .NET DataSet or XML. For information on how to send DataSets from non-.NET clients, please refer to Chapter 5. In fact, this pattern is a perfect opportunity to use another pattern covered in Chapter 4 ”Chained Service Factory. By combining the LCT Server with the Chained (or Unchained ) Service Factory, you can call any synchronous operation asynchronously.

Figure 7.5. LCT Server generic class diagram.

graphics/07fig05.gif

Once Execute() is called on the TransactorService, an identifier will be generated to identify the async job uniquely. This ID will be used to track and manage the async job once the caller is initially unblocked. The algorithm for generating the ID is up to you. You can use a GUID, an ID column from SQL Server, or as you'll see next, a database counter. The only other step before control is returned back to the caller is to save the job. This is where state management comes in. If you are using a formal messaging framework such as MSMQ, you could simply dump the parameterized message to a queue. How the state is held and what transport you use is really implementation-dependent. The only goal you have is to unblock your caller as soon as possible. This, of course, is the whole point of calling something in async fashion ”speed. Once the client is free, the server can normally do what it does to finish processing the request. Instead of using MSMQ, this pattern simply abstracts those operations common to all async job management. These operations can be found defined in the ITransactor interface and are implemented in the TransactorFacade. How they are implemented is again up to you. This pattern leverages off of the .NET Web service proxy generated from VS.NET to call the original operation. As shown in the class model, just before Transactor.Execute returns, the BeginExecute is called on the ExternalService. This will invoke the service asynchronously and register a callback method located in TransactorService. Once the operation is complete, the callback method, which is named appropriately enough ExternalCallback() , is invoked by .NET. Once invoked, the results of the ExternalService operation can be retrieved. Once retrieved, those results are then stored for later retrieval by the client. The client will use the return job ID from the initial Execute call as the lookup identifier for the both the status and the results of the operation. The LCT Server acts as an async delegate (of sorts) that simply delegates the operation payload but it does so asynchronously. The TransactorService acts as a client to the operation that the caller usually invokes synchronously. In fact, the role of the TransactorService is no different than any client. The only two differences in its role are that it acts as a proxy for the real client and drives the TransactorFacade. The TransactorFacade's role is simply to help manage state.

Consequences

The LCT Server pattern has the following benefits and liabilities:

  • Abstracts the implementation of a synchronous model from an asynchronous model . Using this pattern, the loosely coupled Transactor can be easily added to your existing synchronous system without direct impact on the existing system. This is perfect for those systems that have already been implemented and deployed. This patterns acts as a "wrapper" around synchronous methods by providing another layer that can be called by non-.NET clients synchronously. The synchronous request is then immediately made into an asynchronous request on the server. The abstraction also acts as a factory for the synchronous services with an interface that does not directly tie itself to any back-end implementation.

    Challenge 7.1

    As was mentioned earlier, this architectural pattern can be implemented with several technology variances, including the designer deciding some of the following:

    • Should polling be used for determining operation status?

    • If so, how often should polling occur?

    • Can server-side notification be used to update operation status instead of polling?

    • How will state be managed on the server?

    • Will messaging be used?

    • If the job results are too large, does a buffering mechanism need to be designed?

    The implementation section of this pattern covers one particular variance (page 364)

  • Eliminates the need for callback interfaces . When dealing with any asynchronous design, the programmer typically has initially two major choices to make on the model. The first is to employ "callbacks," which will call back to an originating interface when the asynchronous method has completed. The other choice is to poll, and this is what is employed for the LCT pattern. Each has its advantages and disadvantages. Polling is simpler to implement but can be less efficient, depending on how often the server must be polled to obtain a transaction status. On the other hand, callbacks can be more efficient but they require a client system that has the actual ability to receive the callback. This is the biggest drawback of callbacks. For simple peer-to-peer activity, this usually isn't an issue. However, even in single-client implementations , the technology must be there to do the broadcast itself.

    Challenge 7.2

    During implementation, what other technologies can be used from the LCT Transactor Service to the external service while still leveraging the .NET framework's asynchronous features?

    The answer lies in the side note in this chapter (page 362).

    For non-.NET clients communicating with a .NET server, the choices are more limited. When many clients are involved in an asynchronous system, a more sophisticated model is required. Typically, these systems require something such as TIBCO or other broadcast-oriented callback technologies to scale well. Polling really is the "poor man's" version of implementing asynchronous status codes. Polling may be considered a less sophisticated option but it does work, and it works rather simply. For non-.NET clients, polling is the only simple solution without adding messaging software such as MSMQ or TIBCO. The other option is to implement a threading model on the client that "spoofs" the asynchronous call by simply running the synchronous transaction in a background thread.

  • May affect the scalability of the server due to polling . The effect polling has on a server's scalability is simply due to the type of request being made and how often it is made. To limit this effect, it is suggested that a simple status request be made and that it be made sparingly. Many applications can be designed so that polling can take place during idle times on the client, when the client starts up, or during some other typically nonimpact event so that the user will not notice. If this is not the case, it would be better to employ some type of callback or threaded event so that a callback can be implemented. Typically, it is better to provide a callback than to call the server continuously more than a few times a minute unless there are only a few clients. These again are design decisions.

  • May force the need for more than one Web server . Depending on your Web service directory layout and the number of Web servers you can deploy in your architecture or Web forms, this pattern may require an additional server. For larger systems that may have hundreds of calling clients, it may be more effective to employ two Web servers so that the polling and the initial synchronous call to the Transactor Service will not affect the scalability of the external service (see Figure 7.5). Each status request made to the Transactor Server may take away from the processing time used by the external service if both services are on the same machine. This should be a concern if there are numerous clients making frequent requests and should be addressed only for large systems or servers with small memory footprints. Again, this is an infrastructure design decision and should be evaluated before deployment.

Participants

  • Client (same) ” This represents any calling client. The client represents a workstation that does not have .NET installed (e.g., Visual Basic 6.0 fat client). This also is any client that does not have messaging software so that polling and the decision to employ the LCT pattern is the best design option. Any existing client will require minor changes to take advantage of this new service. After the client calls the LCT server, it must also poll for status using the same service. How this is implemented is again design-dependent and should be as unobtrusive to the client's normal workflow as possible.

  • TransactorService(same) ” This is the Web service that will receive all incoming traffic that is called synchronously. Once it receives the invocation (by calling TransactorService.Execute()), its job is to forward the request immediately to an external Web service asynchronously. The implementation of all logic for the TransactorService is implemented in the TransactorFacade to abstract the logic between the Web service and the core business tier .

    Note

    Please note that the external service can be called via ".NET remoting," which may provide better performance, albeit at the cost of some overall system scalability for large implementations. This implementation uses another Web service call but is not tied to this implementation approach. In fact, it is recommended that all controlled server-to-server invocation use .NET remoting, due to its performance benefits. Web services were used here simply for demonstration purposes and ease of implementation.

    This service is actually where the callback function is implemented. Upon completion of the external service, the callback is then used to update the original transaction status, using some persistent data store. This status can be queried or polled at any time by the original client. The client polls the TransactorService by calling TransactorService.GetStatus() repeatedly until a completion status or a discrete error status can be obtained. Once a completion status is received by the client, the client can then call GetTransaction() to receive the final results. The GetTransaction() method is the final call made from the client to obtain any results from the original call. For methods that do not return result sets, the asynchronous design is simplified and may keep the client from polling at all. However, for methods that do return data, polling is required, and GetTransaction() must be called at some point. This is where .NET, and more specifically ADO.NET and XML support, really come into play. Instead of requiring the implementation of different data models for each result set from each individual asynchronous method, the developer can leverage XML schemas. With XML schemas and ADO.NET, the developer can store both the schema and the instance data in one table. This allows the developer to represent almost any result set using a single logical database design. For more information how this pattern works, please refer to Chapter 5's dynamic data model pattern called Poly Model and the Schema Field pattern.

  • ExternalServiceProxy (FinancialServiceFactoryProxy) ” This is the generated or manually created proxy between the TransactorService object and the external service itself. This provides the asynchronous logic between these two entities of the pattern and works like any other proxy, representing an external service as though the service were local to the caller. For this implementation, it is the Visual Studio .NET generated proxy from the external service's WSDL file (from its Web service).

  • ExternalService (FinancialServiceFactory) ” The external service can be any service providing the methods that the client wishes to invoke asynchronously. It can contain any set of method signatures. However, it is recommended that a factory or other standard interface method be used to provide a "contract" between the client and the server so that a single LCT Server can be used. The Execute method is the contract method in this implementation example.

  • TransactorFacade (same) ” As mentioned in the TransactorService participant, this " houses " all logic used by the TransactorService. This used the Web Service Fa §ade pattern as defined in Chapter 4. It implements all methods from the ITransactor interface and any helper methods used by the TransactorService object.

  • ITransactor (same) ” All of the standard methods used by the LCT Server are defined here. These methods are implemented by the TransactorFacade, directly called by the TransactorService, and originally delegated from calls originating from the client.

Implementation

One of the primary benefits of this pattern is its ability to be seamlessly added to the existing application. This is especially true for clients that have not yet installed .NET at the desktop. For the Financial Server application, the LCT Server simply "wraps" the FinancialServiceFactory class and is treated as the ExternalService entity. Now the client must now call the TransactorService synchronously and pass identical information as though calling the FinancialServiceFactory (Figure 7.6). The following Visual Basic Service Client calls the Web service method Execute on the TransactorService as though it were the real FinancialServiceFactory, using identical parameters. The difference lies in the methods that may return result sets. All result sets will not be returned synchronously as before but through the invocation of the GetTransaction(), as explained earlier.

Figure 7.6. LCT Server class diagram ”implementation example.

graphics/07fig06.gif

Listing 7.14 shows a typical FinancialService non-.NET client (implemented in Visual Basic 6.0) calling the FinancialServiceFactory through the LCT Transactor Service:

Listing 7.14 Visual Basic SOAP Toolkit client used for calling the LCT Server.
 Option Explicit    Dim soapClient As MSSOAPLib.SoapClient    set soapClient = CreateObject("MSSOAP.SoapClient")    Dim oNodes As MSXML2.IXMLDOMNodeList    Dim oDataSet As MSXML2.IXMLDOMNodeList On Error Resume Next    . . . 'set up oDataSet    Call soapClient.mssoapinit(       "http://localhost/transactorservice.wsdl",    "TransactorService")    Set oNodes = soapClient.Execute(oDataSet)    If Err <> 0 then       Result = Result & "Err.Number=" & Err.Number       Result = Result & "Err.Description=" & Err.Description       Result = Result & "SoapClient.faultcode=" & sc.faultcode    End If    . . . 'process oNodes xml return dataset (optional) 

The Listing 7.14 VB code snippet is just that ”a snippet. Most unmanaged clients will most likely be more complex. The LCT server is just another Web service. Calling it from VB can either use the SOAP Toolkit's high-level API as shown in Listing 7.14, or for more control, the low-level API can be used. There are only three calls to be made to the LCT server. The first call invokes the Execute call to initiate the transaction. Once called, the remaining calls consist of any polling invocations and the final call to retrieve the results from the initial request. For those transactions that do not return data, these final two invocations are not necessary. As mentioned, one of the benefits of this pattern is its simplicity. The simpler the client is designed, the better. More complicated clients ”such as those that must contain a callback, use multithreading to mimic asynchronous behavior, or utilize a messaging system ”all add deployment configuration headaches . Placing more of the design complexity at the server level will increase a developer's control and provide the ability to make future changes in the asynchronous behavior much easier.

Once called, the Transactor service receives the DataSet input parameters and immediately forwards them to the actual service. The only difference is that the LCT Server becomes a "client" of sorts with that external service, and it is here that the callback must be implemented. The callback, when called, completes the original transaction workflow by updating the status of the transaction from "pending" to "complete" (other status codes can be implemented if needed). The final activity performed by the callback is updating the transaction record with the result set of the external method. The external method, in this case, simply returns a set of records back, and it can contain any data as long as it is encapsulated in a DataSet. The result set is then directly saved to the database by calling TransactorFacade.UpdateTransaction(), as implemented in Listing 7.15.

Listing 7.15 shows the main execution (Execute) call, the callback method, a method for checking the status of the transaction, and the final transaction result set retrieval method.

Listing 7.15 LCT Server "plumbing" implementation.
 /// <summary> /// This is the driver web service for the loosely coupled /// /// transactor /// From this web service, another service can be called /// /// /// asynchronously /// and its status queried and eventually the transaction results /// returned    /// </summary>    public class TransactorService : System.Web.Services.WebService    {    . . .       /// <summary>    /// Executes a particular external service asynchronously,    /// can be easily made to    /// call chained or unchained service factory (chapter 4)    /// to launch any external service       /// </summary>       [WebMethod]       public long Execute()       {          ITransactor oTransactor = (ITransactor)          new TransactorFacade();          long lTransactorId = 0;          localhost.ExternalService oExtService =          new localhost.ExternalService();          try          {             // generate a unique transactor id             lTransactorId =          oTransactor.GenerateTransactionId();             // insert the job into the job table and give       // it a pending status STATUS = 1             oTransactor.CreateTransaction(          lTransactorId, 1);  // 1 for pending             // simply delegate to the       // InvokeTimeConsumingMethod call...       // this can be any call including a       // chainedservicefactory             oExtService.TransactorHeaderValue =          new localhost.TransactorHeader();       oExtService.TransactorHeaderValue.lTransactorId       = lTransactorId;       oExtService.BeginInvokeTimeConsumingMethod(       new System.AsyncCallback(       TransactorService.       ExternalServiceCallback), oExtService);          }          catch (Exception e)          {             throw new          BaseException(          Utilities.BuildErrorMessage(          "LCT Server Exception thrown",          e.Message), true);          }          return lTransactorId;       }       /// <summary>       /// Returns just the status of the original transaction       /// </summary>       [WebMethod]       public int GetStatus(long lTransactorId)       {          ITransactor oTransactor = (ITransactor)          new TransactorFacade();          int nStatus = 0;          try          {             // insert the job into the job table and give       // it a pending status STATUS = 1             nStatus = oTransactor.GetStatus(lTransactorId);          }          catch (Exception e)          {             throw new          BaseException(          Utilities.BuildErrorMessage(       v   "LCT Server Exception thrown",          e.Message), true);          }             return nStatus;          }       /// <summary>       /// Executes a particular external service asynchronously,    /// can be easily made to    /// call chained or unchained service factory (chapter 4)    /// to launch any external service       /// </summary>       [WebMethod]       public DataSet GetTransaction(long lTransactorId)       {          ITransactor oTransactor = (ITransactor)          new TransactorFacade();          DataSet ds = null;          try          {             // insert the job into the job table and give       // it a pending status STATUS = 1             ds = oTransactor.GetTransaction(lTransactorId);          }          catch (Exception e)          {             throw new          BaseException(          Utilities.BuildErrorMessage(             "LCT Server Exception thrown",          e.Message), true);          }          return ds;       }       /// <summary>       /// Asynchronous Callback method for async ops       /// </summary>       /// <param name="ar"></param>       public static void ExternalServiceCallback(IAsyncResult ar)       {          DataSet dsOriginal = new DataSet();          ITransactor oTransactor = (ITransactor)          new TransactorFacade();          try          {             // Obtains return value from the delegate call       // using EndInvoke.             localhost.ExternalService oExtService =          (localhost.ExternalService)ar.AsyncState;             dsOriginal =    oExtService.EndInvokeTimeConsumingMethod(ar);             // update the transaction with a success (==2) // and the results of the original transaction             if (dsOriginal != null)          voTransactor.UpdateTransaction(    oExtService.TransactorHeaderValue.    lTransactorId, 2, dsOriginal);          }          catch(Exception e)          {             throw new          BaseException(          Utilities.BuildErrorMessage(          "LCTServer Exception thrown",          e.Message), true);          }       }    } 

Let's start with Execute(). This is the first method called by the client. The Execute method initially instantiates the external service and later uses its asynchronous interface to invoke its own "contract" method. The external service in this case is another Web service but, as mentioned, can also come in the form of a .NET remoting call. Another Web service was used for simplicity and because the code was already written from the Web Service Factory (Chapter 4). The Web Service Factory Method is named InvokeTimeConsumingMethod and is leveraged and invoked through a single call into that external Web service. This call is delegated directly from the Transactor's Execute call, making this pattern relatively simple to implement.

You'll notice that the external service method is actually indirectly called through the Begin version of the call, as generated by the Web service proxy using Visual Studio .NET. Just before calling the asynchronous factory, however, a transaction ID is generated, and a record is created into the database. The transaction ID generation uses a stored-procedure to generate the ID and SQL/ADO.NET to insert the new record in the database, as well as to update the status. The new database record is given a "pending" status code to provide the means by which the client can now track the status of the transaction. Most of the heavy lifting , such as driving the transaction ID generation and the transaction record creation, is done by the TransactorFacade class and its methods. This code is shown in Listing 5.15.

Note

I also use SOAP headers to pass the transaction ID. This is completely optional because I decided not to pass any input parameters into the external service.

Listing 7.16 Sample functional implementation drive by the LCT Server.
 /// <summary> /// Main driver for all async (LCT) transaction operations /// </summary> public class TransactorFacade : ITransactor {    private string m_sConnectionString = string.Empty;    public TransactorFacade()    {       // for SQL Server Test       m_sConnectionString = "Integrated Security=SSPI;" +          "Initial Catalog=PatternsDotNet;" +          "Data Source=DEV06;";    }    /// <summary>    /// Calls the appropriate stored proc to increment a db ///  count and return it    /// </summary>    /// <returns></returns>    public long GenerateTransactionId()    {       SqlConnection cn = null;       StringBuilder sbExecSQL = null;       SqlCommand oCommand = null;       long lTransactionId = 0;       try       {          sbExecSQL = new StringBuilder(       "exec sp_DNPatternsIncCounter");          sbExecSQL.Append(" 'JOB_ID', 1");          // make the connection          cn = new SqlConnection();          cn.ConnectionString = m_sConnectionString;          cn.Open();          // call the storeproc sp_DNPatternsIncCounter          if (cn.State == ConnectionState.Open )          {             oCommand = new       SqlCommand(       sbExecSQL.ToString(), cn);                      lTransactionId =       (long)oCommand.ExecuteScalar();          }       }       catch (Exception e)       {          throw new       BaseException(       Utilities.BuildErrorMessage(       "General Exception thrown",       e.Message), false);       }       finally       {          cn.Close();       }       return lTransactionId;    }    /// <summary>    /// This creates a new transaction    /// as part of a transaction table (uses the specified /// transactor id)    /// </summary>    /// <param name="lTransactorId"></param>    /// <param name="nStatus"></param>    /// <returns></returns>    public DataSet CreateTransaction(long lTransactorId,    int nStatus)    {       DataTable oTable = new DataTable();       SqlParameter oParam = null;       string sSelectText = string.Empty;       SqlConnection cn = null;       DataSet ds = null;       SqlDataAdapter da = null;       try       {          // make the connection          cn = new SqlConnection();          cn.ConnectionString = m_sConnectionString;          cn.Open();          if (cn.State == ConnectionState.Open )          {             // build the sql statement and prepare    // the dataset             da = new SqlDataAdapter(       "SELECT * FROM JOB", cn);             ds = new DataSet("JOB");             da.Fill(ds, "JOB");             // prepare a data adapter with insert    // statement             string sInsertCommand = "INSERT INTO JOB       (JOB_ID, STATUS) VALUES       (@JOB_ID, @STATUS)";             da.InsertCommand = new       SqlCommand(sInsertCommand, cn);             // bind parms to insert             oParam = new SqlParameter("@JOB_ID",       SqlDbType.Int);             oParam.SourceColumn = "JOB_ID";             oParam.SourceVersion =       DataRowVersion.Current;       da.InsertCommand.Parameters.Add(oParam);             oParam = new SqlParameter("@STATUS",       SqlDbType.Int);             oParam.SourceColumn = "STATUS";             oParam.SourceVersion =       DataRowVersion.Current;       da.InsertCommand.Parameters.Add(oParam);             oTable = ds.Tables["JOB"];             if (oTable != null)             {                // insert the new row                DataRow oNewRow = oTable.NewRow();                oNewRow["JOB_ID"] = lTransactorId;                oNewRow["STATUS"] = nStatus;                oTable.Rows.Add(oNewRow);                // save it to the database                da.Update(ds, "JOB");             }             else                throw new                   BaseException(          Utilities.BuildErrorMessage(                      "Inserting Job Failed",          "Table object was null"),       false);          }       }       catch (Exception e)       {          throw new       BaseException(       Utilities.BuildErrorMessage(       "General Exception thrown",       e.Message), false);       }       finally       {          da.Dispose();          cn.Close();       }       return ds;    }    /// <summary>    /// Updates the transaction with the dataset results    /// of any returned external call    /// </summary>    /// <param name="lTransactorId"></param>    /// <param name="nStatus"></param>    /// <returns></returns>    public DataSet UpdateTransaction(long lTransactorId,    int nStatus, DataSet dsOriginal)    {       DataTable oTable = new DataTable();       SqlParameter oParam = null;       string sSelectText = string.Empty;       SqlConnection cn = null;       SqlDataAdapter da = null;       DataRow oRow = null;       DataSet dsJob = null;       try       {          // make the connection          cn = new SqlConnection();          cn.ConnectionString = m_sConnectionString;          cn.Open();          if (cn.State == ConnectionState.Open )          {             // build the sql statement and prepare    // the dataset             da = new SqlDataAdapter(                new StringBuilder(          "SELECT * FROM JOB WHERE JOB_ID = ")       .Append(lTransactorId).ToString(),       cn);             dsJob = new DataSet("JOB");             da.Fill(dsJob, "JOB");             // build the update sql command             da.UpdateCommand = new SqlCommand(       new StringBuilder(          "UPDATE JOB SET STATUS = @STATUS, DATA_SCHEMA =       @DATA_SCHEMA, DATA = @DATA WHERE JOB_ID = @JOB_ID")       .ToString(), cn);             // bind the insert parameters             oParam = new SqlParameter(       "@STATUS", SqlDbType.Int);             oParam.SourceColumn = "STATUS";             oParam.SourceVersion =       DataRowVersion.Current;       da.UpdateCommand.Parameters.Add(oParam);             oParam = new SqlParameter(       "@DATA_SCHEMA", SqlDbType.Image);             oParam.SourceColumn = "DATA_SCHEMA";          voParam.SourceVersion =       DataRowVersion.Current;       da.UpdateCommand.Parameters.Add(oParam);             oParam = new SqlParameter(       "@DATA", SqlDbType.Image);             oParam.SourceColumn = "DATA";             oParam.SourceVersion =       DataRowVersion.Current;       da.UpdateCommand.Parameters.Add(oParam);             oParam = new SqlParameter("@JOB_ID",       SqlDbType.Int);             oParam.SourceColumn = "JOB_ID";             oParam.SourceVersion =       DataRowVersion.Current;       da.UpdateCommand.Parameters.Add(oParam);             da.Fill(dsJob, "JOB");                // grab the table so we can update each       // field as bound by the parameters                oTable = dsJob.Tables["JOB"];                if (oTable != null)                {                   // should be first and only row          // found                   oRow = oTable.Rows[0];                   if (oRow != null)                   {                      // insert the new row                      oRow["STATUS"] = nStatus;                      if (dsJob != null)                      {          oRow["DATA_SCHEMA"] =          Utilities.ToByteArray(                DsOriginal          .GetXmlSchema());                      oRow["DATA"] =          Utilities.ToByteArray(             dsOriginal.GetXml());                   }                   // save it to the database                   da.Update(dsJob, "JOB");                }                else                   throw new BaseException(...);             }             else                throw new       BaseException(. . .);          }       }       catch (Exception e)       {          throw new BaseException(. . .);       }       finally       {          da.Dispose();          cn.Close();       }       return dsJob;    }    /// <summary>    /// This will regenerate the original DataSet from the /// stored schema and data as xml in the job table    /// </summary>    /// <param name="dsJob"></param>    /// <returns></returns>    public DataSet ExtractOriginalData(DataSet dsJob)    {       DataSet dsOriginal = new DataSet("OriginalData");       string sSchema = null;       string sData = null;       try       {          // first convert byte array to string          if (dsJob != null)          {             sSchema = Utilities.FromByteArray(       (byte [])dsJob.Tables["JOB"]       .Rows[0]["DATA_SCHEMA"]);             sData = Utilities.FromByteArray(       (byte [])dsJob.Tables["JOB"]       .Rows[0]["DATA"]);             dsOriginal.ReadXmlSchema(       new StringReader(sSchema));             dsOriginal.ReadXml(       new StringReader(sData));          }       }       catch (Exception ex)       {          throw new BaseException(. . .);       }    return dsOriginal;    }    /// <summary>    /// Returns the transaction as a dataset    /// for the transactor id    /// </summary>    /// <param name="lTransactorId"></param>    /// <returns></returns>    public DataSet GetTransaction(long lTransactorId)    {       string sSelectText = string.Empty;       SqlConnection cn = null;       SqlDataAdapter da = null;       DataSet ds = null;       DataSet dsOriginal = null;       try       {          // make the connection          cn = new SqlConnection();          cn.ConnectionString = m_sConnectionString;          cn.Open();          if (cn.State == ConnectionState.Open )          {             // build the sql statement and prepare    // the dataset             da = new SqlDataAdapter(                new StringBuilder(             "SELECT * FROM JOB WHERE JOB_ID = ")          .Append(lTransactorId)       .ToString(), cn);             ds = new DataSet("JOB");             da.Fill(ds, "JOB");             // just so we can see the stored bytes    // array as returned xml data             dsOriginal = ExtractOriginalData(ds);          }       }       catch (Exception e)       {          throw new BaseException(. . .);       }       finally       {          da.Dispose();          cn.Close();       }       return dsOriginal;    }    /// <summary>    /// Returns the status of the transaction    /// </summary>    /// <param name="lTransactorId"></param>    /// <returns></returns>    public int GetStatus(long lTransactorId)    {       string sSelectText = string.Empty;       SqlConnection cn = null;       SqlDataAdapter da = null;       DataSet ds = null;       int nStatus = 0;       try       {          // make the connection          cn = new SqlConnection();          cn.ConnectionString = m_sConnectionString;          cn.Open();          if (cn.State == ConnectionState.Open )          {             // build the sql statement and prepare    // the dataset             da = new SqlDataAdapter(             new StringBuilder(             "SELECT * FROM JOB WHERE JOB_ID = ")          .Append(lTransactorId).ToString(),       cn);             ds = new DataSet("JOB");             da.Fill(ds, "JOB");             // just so we can see the stored bytes       // array as returned xml data             nStatus = (int)ds.Tables["JOB"]       .Rows[0]["STATUS"];          }       }       catch (Exception e)       {          throw new BaseException(. . .);       }       finally       {          da.Dispose();          cn.Close();       }       return nStatus;    } } 

By placing the logic in this fa §ade, you have the obvious advantage of directly calling that class without going through the Web service (see Web Service Fa §ade pattern in Chapter 4). Each method in this TransactorFacade should be relatively self-explanatory. Each implements one of the ITransactor interface methods and makes up the "heart" of the pattern. UpdateTransaction() requires the most explanation because this is the method that is called by the Transactor Service callback when the asynchronous external service has completed. The two mains items that are updated are the status and the result set that was returned from the original external service call. The dataset is strongly leveraged here in that both the instance data from the data and the schema are stored. Each is stored in its own field and allows any data to be stored; the schema can then be retrieved at any time, along with the record. Both the instance data and the schema are stored as "image" fields in SQL Server. If using Oracle, BLOBS could also be used. To store each field, the DataSet XML contents are first converted to a byte array and directly saved using ADO.NET. For more details on using this form of data storage, please refer to Chapter 5 on persistence-tier patterns. The only other method needing explanation is ExtractOriginalData(). This is a helper method used once GetTransaction() is called on the TransactorService, and the database contents are then returned as a complete DataSet.

Like its name, this pattern provides the implementer with a lot of flexibility in the manner in which it is implemented. Using several design variances, it can have many uses and can even function from server-to-server invocation, as well as client to server. It also represents an assembly of several best practices and patterns presented in this book. When it comes to providing any asynchronous solution, the model to live by is "the simpler, the better," and I hope this pattern delivers on that premise .



.NET Patterns. Architecture, Design, and Process
.NET Patterns: Architecture, Design, and Process
ISBN: 0321130022
EAN: 2147483647
Year: 2003
Pages: 70

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net