Applying .NET Patterns


Up to this point, I've provided background on what Company ABC does and the products it produces, and I have given you an overview of the architecture. Now I will turn my attention to the patterns I used to create the architecture. Not all of the patterns covered in this book will be discussed here but I will talk about the most important ones. I'll even try to go over the order in which the patterns were created and how they were implemented in code. I will give an overview of the process patterns we used to come up with the design and architecture patterns themselves , as well as some of the implementation options we employed.

Applying the Service Fa §ade Pattern

One of the challenges of creating a framework, or any application for that matter, is trying to figure out where to start. Designing a generic framework without a specific product to reside on it presents even a larger challenge. Fortunately, I had a product to target and even a legacy framework (the C++/COM framework) to model the new one on. Given that I probably could have started designing any piece first, like most designers, I begin with the most obvious ”the point of entry into the architecture. The Service Fa §ade was the pattern I designed and implemented first. The Service Fa §ade, as discussed in Chapter 4, is what Microsoft calls a service interface . It provides the contract with the outside world. Other than the Web Service Factory (which I will cover in the next section), it represents the initial entry point into the framework, and its logic drives all other services. In fact, because I knew that Web services would be used, I wanted to create an entity that would be friendly to Web service clients, memory-managed clients, and even unmanaged clients in some aspects. Before creating the Web service, I also needed an object to instantiate with the first Service Fa §ade designed providing me this. For more background on Service Fa §ade, Service Interface, and Service Agents, please review Chapter 4 or visit Microsoft's own patterns and practices Web site at: www.microsoft.com/practices/.

The way the Service Fa §ade was created was based on the fact that it would be called from a Web service (thus the word service ) and the fact that it drives one area of the framework. This area is also considered a service, so the pattern name plays a dual role. The first area or service of the framework I was to accommodate was payment processing. This is the core of the Commercial Framework and will support the transaction activity of the system (e.g., credit card authorization, electronic check entries, etc.). Other services include things such as reporting, batching , workflow, or other main business activities. Each service would have a predefined name and eventually be made up of one of the parameters used in the decision schema described earlier. Using the service name and predefined naming standard allowed me to instantiate any Service Fa §ade in a late-bound fashion using Reflection.

Coupled with a standard entry point method signature, the Service Fa §ade could be created by a client using a simple string or, in this case, an XML schema. Listing 6.2 is a snippet from the Web Service Factory (see the section on implementing the Unchained Service Factory later in this chapter for details). It shows how the service name is read from an incoming packet (DataSet dsPacket, in this case) and used to instantiate the appropriate Service Fa §ade. The Service Fa §ade instantiated below is called PaymentFacade , and the method invoked is called Execute . The parameter passed to Execute is simply the raw DataSet passed in this method from the client. That way, this acts simply as a generic factory and does not care which business schema or parameter list was used, other than what service was required.

Listing 6.2 Reflection code snippet used in the Service Factory for late-bound action.
 . . . DataSet ds = new DataSet();    string sService = PacketTranslator.GetService(dsPacket);    Assembly oAssembly = Assembly.GetExecutingAssembly();    string sTypeName = . . . // build object string    Type oServiceFacadeType = oAssembly.GetType(sTypeName);    object oServiceFacade =    Activator.CreateInstance(oServiceFacadeType);    MethodInfo oFactoryMethod =    oServiceFacadeType.GetMethod("Execute");    object[] oaParams = new object[2];    oaParams[0] = dsPacket;    oaParams[1] = false;    ds = (DataSet) oFactoryMethod.Invoke(oServiceFacade, oaParams); . . . 

From this point, it is the Service Fa §ade's job to decipher further how to route the request and what business product should be created deeper into the framework. Listing 6.3 is a stripped-down version of the PaymentFacade's constructor and Execute method. Much of reusable servicing of this class is in the parent Fa §ade.

Here you can see how the Execute is called with the original DataSet and delegated to the Product Manager (CreateProduct method). Before CreateProduct() is called, however, an Abstract Packet is prepared from the raw DataSet in the PreparePacket method because the Product Manager understands only Abstract Packets. It is here that the packet is translated (see Packet Translator from Chapter 4), and its driver code is shown in Listing 6.3.

Listing 6.3 A sample Service Fa §ade called PaymentFacade for handling all payments.
 public class PaymentFacade : Facade    {       . . .       public override DataSet Execute(       DataSet dsPacket, bool bCache)       {          Packet oPacket;          PreparePacket(dsPacket);          . . .    if (Product == null){             Product = CreateProduct(GetPacket());}    if (bCache)          {             Product.PrepareCache();          }          else          {       Product.Packet = GetPacket();    }          oPacket = Product.Execute();          . . . // return raw packet back to caller          return PreparePacket(oPacket);    } . . . 

The following factory method CreateProduct() instantiates the proper Product Manager for the requested service in the Service Fa §ade. Using a switch case construct and a "type" string to identify the correct Product Manager, the Service Fa §ade can then delegate the call and its packet to the appropriate business object. The type string is set during packet preparation in the Service Fa §ade, which is originally passed in the decision schema by the client. From here, the Product Manager takes over and further delegates responsibility to its own child objects as needed. In short, the decision schema elements determine how the request is routed using a service string to create the Server Fa §ade, the packet type to create the Product Manager, and finally the Action string to invoke the correct method within the Product Manager (see the Product Manager section below and in Chapter 4).

The parameters, passed by the client, are prepared and sent through to the recipient business object as needed. Usually the parameters are untouched by the rest of the framework, and only the decision schema is read so that the request can be routed. This is crucial to the architecture and abstracts the Service Fa §ade from having any business-specific behavior coupled within it. The specific parameters can be contained in a unique schema designed specifically for that one business object or objects. For electronic checking (ProductX), there is a product schema and for credit card payments (ProductY), there is a schema designed to house its own specific parameters. The Service Fa §ade is simply a vanilla conduit to facilitate this creation and routing between a client and product itself.

Listing 6.4 The Product Manager factory method for creating the correct product object.
 public ProductManager CreateProduct( Packet oPacket)    {       . . . ProductManager oProductManager;       switch (oPacket.Type)       {          . . .          case Constants.PRODUCTY_TYPE:             oProductManager = ( ProductManager) new       ProductY(oPacket);             break;          case Constants.PRODUCTX_TYPE:             oProductManager = ( ProductManager) new       ProductX(oPacket);             break; . . .          default:             oProductManager = null;             break;       }       return oProductManager;    }    . . . 

One thing that should be noted is that the code snippet in Listing 6.4 shows the Product Manager being instantiated as an early bound object. The next version of this architecture will facilitate a late-bound one in the Product Manager itself (recall only the Unchained Service Factory piece is late bound using Reflection). This mode will work similarly to the Unchained Service Factory and will allow the Product Manager to be late bound using .NET Reflection, as shown in this section.

Applying the Product Manager Pattern

Now that I've provided the perfect segue to the Product Manager pattern, let me discuss how we went about implementing it for ProductX. The following abstract class, appropriately named ProductManager , contains the generic methods used by all child classes of Product Manager. For ProductX, things such as packet preparation, schema retrieval, instrumentation, etc., can be abstracted from the child class and placed in the parent class.

The following shows only a few of the methods used by ProductX and the child class used for electronic check processing. The other element of the following code snippet defines the "contracted" method that all child Product Managers must implement. The Execute method must be implemented so that the Service Fa §ade can generically delegate all action to the business object. Other standardized method signatures can also be added so that all products adhere to the framework. The other standardized methods added to this product manager were GetProductSchema(), PrepareCache(), and PreparePacket(). All must be implemented by each Product Manager child class so the Service Fa §ade or any external client can retrieve the specific XML schema associated with the particular product and can do so in a "contracted" standard fashion. Because different products implement different schemas and their means of access may vary, this method should be handled by the product whose schema is requested. PrepareCache() does what it sounds like and performs whatever expensive operations that are common and at the same time can be held in memory for the next invocation/round trip. For more on caching, please refer to the Abstract Cache pattern featured in Advanced Patterns section of Chapter 7. The PreparePacket method is meant to allow the product to manipulate the packet so that it has a chance to retrieve itself from cache if that is an option. This is a very generic "contract" and will vary greatly, depending on the business implementation. The PreparePacket method is used to determine whether a primary key has been generated for the transaction during a single save operation.

For this architecture, I use a database counter to control primary key values from either Oracle or SQL Server. It is in this method that a primary key can be generated, provided to the packet, and eventually saved if that is the requested operation. This is typically database access 101 and only frames the implementation discussion here. How you determine uniqueness in your packet (using something such as database counters) is up to you. The point is first to define all contract methods that all children of Product Manager must implement to avoid refactoring headaches down the line.

Listing 6.5 The contract interface for one Product Manager implementation.
 public abstract class ProductManager {    . . .    protected Packet PrepareSchema(string sType)    {       CommonData oData = null;       Packet oPacket = null;       . . . if(Packet != null)       {          oData = DataFactory.CreatePolyData();          if(oData != null)          {             . . . oPacket = oData.GetProductSchema(sType);             . . .          }       }       return oPacket;    }    public abstract Packet Execute();    public abstract Packet GetProductSchema();    public abstract void PrepareCache();    public abstract bool PreparePacket( Packet oPacket);    . . .    public bool PreparePacket()    {       . . . 

There are two things worth pointing out in the code snippet for Listing 6.6. Here is an example of only one product in the Product Manager hierarchy that could be implemented. This happens to be our product, ProductX. The constructor, among other initialization activity, reads the passed-in Abstract Packet and provides the DataSet that the packet wraps for convenience. How the packet is passed to the product is product-specific, and this is only one interpretation. The only other item I should point out is the call to GetProductSchema() in the PrepareCache().

For a schema-based system such as the Product Manager, it would be wise to cache all schemas. XML Product Schemas should change infrequently enough to justify them sitting in memory for a few hours, thus speeding up repetitive calls. Remember that those schemas are usually going to be stored in a database somewhere. If you can cache that activity, you will obviously save a database round trip later on. If caching is used in a Web form, then I highly recommend using something outlined in "Use Data Caching Techniques to Boost Performance and Ensure Synchronization" by David Burgett, published in MSDN Magazine , December 2002. Burgett clearly explains some best practices for data caching in a Web form environment and goes into the means of updating your cache from server to server to ensure data consistency. All back issues of MSDN Magazine can be viewed online at http://msdn.microsoft.com/msdnmag/default.aspx.

Listing 6.6 Code snippet from a sample Product object (ProductX).
 public class ProductX : ProductManager    {       public ProductX(Packet oPacket)       {          . . .    Packet = oPacket;          Data = (ProductSchema) Packet.RawData;          . . .       }       public override void PrepareCache()       {          . . .    if (CachedSchema == null)             CachedSchema = GetProductSchema();          . . .       } 

The heart of the Product Manager derived product is the Execute function. Here you can see that this is where further ProductX routing takes place for this business object. The Packet.Action property, which was set during packet translation, is used to call the appropriate ProductX method. All arguments will be passed in the Product Schema's instance data and will be read by the routed business operation if necessary. The Execute method can very easily grow too large and may require the use of a Strategy, Fa §ade, or Chain of Responsibility (GoF) pattern to decouple some of this functionality among different "experts."

Listing 6.7 Factory method implementation of the Product object.
 public override Packet Execute()    {       Packet oPacketOut = null;       Operation = Packet.Action;       . . . switch (Packet.Action) {          case GET_PRODUCT_SCHEMA_ACTION:          {             . . .             oPacketOut = GetProductSchema();    . . .             break;          }          . . .          case ProductX_SAVESCHEDULE_ACTION:    {                . . .                oPacketOut = SaveTransaction(false);                . . .                break;          }          case ProductX_VALIDATEPACKET_ACTION:          {             . . .             ValidatePacket(Packet);             . . .             oPacketOut = Packet;             break;          . . .          return oPacketOut; . . . 

The snippet in Listing 6.8 from the ProductX class shows the use of PrepareSchema from the parent Product Manager. It checks the cache to determine whether one already exists in memory and, if so, uses it.

Listing 6.8 A code snippet for retrieving Product Schema from ProductX.
 public override Packet GetProductSchema() {    Packet oPacketOut = null;    if (CachedSchema == null) {       . . . oPacketOut = PrepareSchema(PRODUCT_SCHEMA);    }    else    {       . . . oPacketOut = CachedSchema;    }    return oPacketOut; } . . . 

The following functions are product-specific yet generic enough to be implemented in the main ProductX product class. Implementing any code in the main Product Manager child is optional because this object can be used solely for routing. These methods can represent any business service; these are just specific to ProductX. Keep the Service Fa §ade premise (or Service Interface premise ) in mind when designing your product business object. ProductX is only a driver of functionality. How much code is placed in this class depends on the complexity of your business service.

For most complex business operations, especially those that must be coordinated as part of a lengthy workflow, it would be wise to keep the main product object as lean and abstract as possible. The driver of a Product Manager inherited child class should never know what is going on beneath the scene and should have only one or two driver methods that it's coupled against. All business details should be implemented and handled as abstractly as possible so that, if need be, the product object itself could be called directly without much knowledge of its inner workings. This provides the best head start in the more complicated design world of workflow orchestration driven by EAI products such as Microsoft Biztalk.

 public Packet ReceiveFile(){. . .} public Packet CleanupFiles(){. . .}    public Packet DeleteIndex(){. . .} 

The next method is the most commonly used ProductX operation because it is used to save the ProductX entry into the database. This action occurs when the user gathers the user 's checking account information and other transaction information so that it can be saved to the database for later scheduling. Once scheduled, the transaction will be processed in batch mode to whatever financial processor has been configured for that installation. The Packet property is sent to other helper functions to validate, prepare, and eventually save the packet contents to the Poly Model-driven database. This is a good example of the typical packet in and packet out operations using the Product Manager.

Listing 6.9 One sample Product object driver implementation of a database save routine.
 public Packet SaveTransaction(bool bValidate)    {       Packet oOutPacket = Packet;       try       {          if(Data == null)             throw new Exception(. . .)    if (bValidate)             ValidatePacket(Packet); . . .    PreparePacket(Packet);          . . .    if (!PolyModel.StoreTransaction(Packet))             throw new Exception(. . .)          oOutPacket = Packet;       }       . . .       return oOutPacket;    } 

The ValidatePacket() called from SaveTransaction() delegates its responsibility to aggregated child objects called Merchant and Processor . The merchant aggregated object is also used during a "collect or aggregation" type operation, which is used to format the final ProductX output before sending it to the processor (Listing 6.10). The Processor object is created using the ProcessorFactory, and the Validator interface is then use to validate the packet's contents. This serves as server-side validation, with exceptions being thrown for invalid parameters such as an invalid account number.

Listing 6.10 Sample code snippet for server-side validation using the Product object.
 public bool ValidatePacket( Packet oProdPacket) {    bool bValid = false;    try    {       . . .       Packet oMerchantPacket = GetMerchant();       . . . Validator = (IValidate) ProcessorFactory.CreateProcessor( oMerchantPacket, oProdPacket); if (Validator == null)       throw new Exception(. . .) return Validator.ValidatePacket(oProdPacket);    }    . . . } 

PreparePacket(), previously referenced, checks to see whether the packet has a transaction ID already assigned to the incoming ProductX entry. If it does not, the transaction ID is generated, and the packet is assumed to be a new record. If a transaction ID were to be read, the packet operation can typically be assumed to be an update. Again, this behavior is specific to this product. Other implemented product classes may "prepare packet" using a different set of business rules.

Listing 6.11 A brief look at preparing packets and driving the financial output from ProductX.
 public override bool PreparePacket( Packet oPacket) {    try    {       if(Packet != null)       {          string sTransID = GetCurrentTransID();          . . .          if (sTransID == string.Empty)          {             . . .          vsTransID = GenerateTransID()          }          oPacket.TransID = Convert.ToInt64(sTransID);          . . .          return true;       }       else          return false;       }       . . .    } } public Packet Collect(bool bSend) {    . . .    // build the packet here...    return oOutPacket; } 

I've included a visual workflow of the ProductX object involved in producing a ProductX file (Figure 6.4). The ProductX file is the content sent to the processor and is used to complete an end user's financial transaction. Each object in the ProductX product (e.g., File, Batch, Record, etc.) plays a part in the final file preparation. The entity details are abstracted from the Product Manager but as you can see, they are coordinated by the main ProductX product object.

Figure 6.4. ProductX business objects (driven by the Product Manager) for transaction output preparation.

graphics/06fig04.gif

As you can see, the Product Manager can be designed in slightly different ways, depending on the requirements. The focus of this pattern is to handle XML schema packets so as to route, describe, and control all business operations within the framework. It defines the operation skeleton for all products in the framework so that they better conform to the framework standards and place as much of the business logic where it belongs ”in the product class.

Applying the Unchained Service Factory Pattern

When originally embarking on the migration from COM to .NET, one of the first conceptual solutions involved using Web services. After all, it is the Web services technology that initially demands your attention and not all of the goodies you receive with the framework. Also, once the Product Manager was complete, the next design and implement task was to create a more robust entry point into the Commercial Framework. Fortunately, wrapping my initial framework in Web services was rather simple, thanks to Visual Studio .NET. With attributes, it was rather easy to provide this type of interface quickly. However, once the technology was proven, what was still needed was a more flexible and robust service interface to make the Commercial Framework more invokeable from a greater number of clients.

After all, I wanted a single entry point into the system for several reasons. First, there were security concerns with publishing every callable business method as a Web service and the difficulty of managing multiple public interfaces. Second, scalability issues were a factor. Throttles had to be placed on methods that could be invoked across the Internet and where bottlenecks could lie. Third, I wanted the Commercial Framework to be used as it was intended, meaning that no one should be able to invoke a low-level business service without going through one or two entry points into the system. This insures that all aspects of the framework are obeyed and that other hidden pieces of the architecture are still being executed correctly. Finally, I wanted the public interfaces into the framework to be flexible.

Once these Web services were published, I didn't want Web service clients to have to alter their own bindings once any public Web method's signature was altered . This included the addition of brand-new service coverage. I wanted a framework that could withstand these types of back-end changes. Through the use of self-describing parameters, I wanted the business methods to be invoked so that the same signature could be used for any callable service within the framework. Using a meta-language ; flexible, self-describing data types such as the DataSet; and XML schemas, the Unchained Service Factory became a viable design.

As mentioned, everything or nearly everything should flow through one or two entry points into the framework. However, there are instances where exceptions need to be made. Exceptions to the rule are made on a case-by-case basis and are judged mostly by both the frequency of the requested service and the odds of its signature ever changing. As you'll see in the example code below, most business activity must flow through one entry point, Execute( ). This serves as the initial public call into the Commercial Framework but means that any back-end business method can be invoked through it. Because of its flexibility, making this call does require some parameter preparation so that the parameters passed can be routed to the appropriate product manager and the framework can respond accordingly . One of these preparations is that of using an XML schema to describe and house the instance data that will be passed into the Web service Execute method. This schema must come from someplace, whether it's persistently stored or retrieved dynamically. For those clients who do not yet have a recent copy of a viable business schema, another publicly callable method is provided to give them just that. Enter the GetSchemaTemplate() method.

This public Web method provides the clients with a callable interface so that any product schema can be retrieved and used to prepare the business method invocation that would ensue. Does this mean this has to be called every time a business method needs to be invoked? Absolutely not! It does mean that those clients may want to cache the schema or be relatively assured that the schema will not change often. If caching is not available and the product schemas change very frequently, this will have to be called each time. Hopefully, neither of the latter situations will be common because one network round trip is obviously better then two.

The GetSchemaTemplate method can be called using any one of four public entry points into the Commercial Framework. It can be called directly using GetSchemaTemplate(). This method then calls EBInvoke(). EBInvoke() calls into the Company ABC framework using an early-bound means of invoking a business service. This is a departure from the "late-bound" access offered by the Unchained Service Factory in this section. EBInvoke() is early bound to the business object it instantiates and will be slightly faster yet less flexible than its late-bound counterpart . LBInvoke() could also be called externally because it directly invokes the Company ABC framework in a flexible late-bound fashion and is the driver method for the Unchained Service Factory. The final external method is just the Execute method itself that simply delegates to LBInvoke(). The GetSchemaTemplate method is provided as a convenience for the caller. If the client so chooses, only Execute() could be called for all services. Here, GetSchemaTemplate() delegates to EBInvoke() for performance reasons only.

As alluded to in Chapter 4 and introduced as part of the Company ABC framework here, the Execute call serves as that single entry point into the system. In the following code snippet (Listing 6.12) you can see some of the implementation behind Execute(), GetSchemaTemplate(), and the two other entry points into the Company ABC framework.

Listing 6.12 A typical Web method that can be called outside the service factory yet still leveraging it in its implementation.
 [WebService(. . .)]    public class WebServiceFactory : WebService    {       . . .       [WebMethod]    public DataSet GetSchemaTemplate(string sType, bool bUseCaching)       {          . . .          try          {             SetMetaInfo(dsDec, sType, GET_SCHEMA_ACTION);             . . .    if (bUseCaching)                ds = EBInvoke(dsDec, true);             else                ds = EBInvoke(dsDec, false);          }          . . .          return ds;       } . . .    [WebMethod]    public DataSet Execute(DataSet dsPacket)       {          DataSet ds = new DataSet();          ds = LBInvoke(dsPacket);          return ds;       } 

The major difference between the helper methods EBInvoke and LBInvoke are how the fa §ades are constructed and called. EBInvoke uses a traditional static "factory method" style, using a switch/case statement to route and create the requested business service by creating the appropriate Service Fa §ade class. Once created, a standard method called Execute (once again) is called on the Service Fa §ade, and the framework delegates the call to the Service Fa §ade. From there, the Service Fa §ade can control where the request should be further routed, if at all. Typically, the Service Fa §ade will route the request to an appropriate Product Manager class, as mentioned earlier in this chapter.

Listing 6.13 From the Chained Service Factory, this code snippet shows the static form of a factory.
 [WebMethod] [TraceExtension()] public DataSet EBInvoke(DataSet dsPacket,    bool bCacheObject) { . . .    try    {       sService = GetService(dsPacket);       switch (sService)       {          case PAYMENT_SVC:             . . .    oFacade = ( Facade) new    PaymentFacade();             . . .             break;          case Constants.REPORT_SVC:             . . .    oFacade = ( Facade) new    ReportFacade();             . . .             break;          default:             return ds;       }    . . .    ds = oFacade.Execute(dsPacket, bCacheObject); return ds; } 

The LBInvoke entry point method, on the other hand, is the default means of calling into the framework. This method is delegated by the main Execute method and uses Reflection in a late-bound fashion (thus the LB notation) to instantiate and call a factory method on any Service Fa §ade. This is a completely generic function that allows any Service Fa §ade to be called, as long as it follows the naming convention. The naming standard used for the Service Fa §ade is up to you.

The decision schema passed into this method will help determine where to route the request. Although this form of entry into the system is late bound and therefore slower, it provides a great deal of flexibility. This flexibility allows any Service Fa §ade to be added to the system at any time without affecting the public Web method interface, thus not affecting any bound clients. How much slower is this method, compared with the early bound version (EBInvoke)? The performance difference is only a few milliseconds slower, and I believe the benefits outweigh the disadvantages in this case. That is why the late-bound version is the only one defaulted and delegated by the call to Execute().

Listing 6.14 This is the late-bound version of Listing 6.13 using Reflection.
 [WebMethod]    [TraceExtension()]    public DataSet LBInvoke(DataSet dsPacket)    {       . . .    // see UnChained Service Factory for locals       try       {          sService = GetService(dsPacket);          oAssembly = Assembly.GetExecutingAssembly();          string sTypeName = . . . // build object string          oServiceFacadeType =       oAssembly.GetType(sTypeName);          oServiceFacade =       Activator.CreateInstance(       oServiceFacadeType);          oFactoryMethod =       oServiceFacadeType.GetMethod(       FACTORY_METHODNAME);          . . .          ds = (DataSet)       oFactoryMethod.Invoke(oServiceFacade,       oaParams);       }       return ds;    } 

In Chapter 4 we covered in great detail the design and implementation of this pattern. With the Commercial Framework, I did not stray much from the template code cover in that chapter, and it follows the template code included with this book to the letter. Providing a generic entry point for any framework is crucial, and this pattern provides only one implementation example of how this was made flexible yet effective.

Applying the Poly Model Pattern

Showing all of the means by which the Commercial Framework employs the Poly Model could take up a chapter of its own. In this section, I would like to show you the main drivers of the Poly Model and some of its key elements because Chapter 5 goes into enough detail on the structure not to be repeated here. Some of the key elements include the four main pieces of the Poly Model, the data factory, the database I/O layer, the generic Poly Model (storing/retrieving schemas), and a little about the indexing used for queries. So let's dive right in.

I use the storage of a ProductX transaction as an example and perfect segue into the Poly Model. In Listing 6.15, I have presented a code snippet from the ProductX Product Manager that drives the Poly Model. You should recognize this method because it was covered earlier. Only the critical calls that relate to the Poly Model are shown. Here the Data Factory is used to create the correct Poly Model object and its StoreTransaction method invoked to store a ProductX transaction. Let's first follow the call to the Data Factory.

Listing 6.15 A typical save operation driver.
 public Packet SaveTransaction(bool bValidate)    {       Packet oOutPacket = Packet;       try       {          . . .       PreparePacket(Packet);       if(!DataFactory.CreatePolyData().StoreTransaction(Packet))          return oOutPacket;    } 

As mentioned earlier, the Data Factory uses a Factory method to instantiate the correct Poly Model object based on both configuration settings and any defaults that may be set. The Data Factory in this case will create the correct Poly Data object such as in Listing 6.16.

Listing 6.16 The factory method of the Data Factory.
 static public PolyData CreatePolyData()    {       if(GetDBType() == DataConstants.DBTYPE.SQL)          return new PolyDataSQLServer();       else if(GetDBType() ==       DataConstants.DBTYPE.ORACLE)          return new PolyDataOracle();       return null;    } 

The PolyData object returned is determined by calling GetDBType(), which uses a configuration setting to determine whether Oracle or SQL Server Poly Data object should be used. Only one installation can be assigned one Poly Data for this particular scenario. For scenarios where a mix of data stores are used, an alternate means of determining the Poly Data object would need to be created.

The PolyData class is the parent class for any database-specific Poly Model class. This parent class acts as the client interface for any Poly Model interaction. The Poly Data then uses other components from the Poly Model to access the database or to build Poly Model queries. The database-specific SQL commands are controlled by the child classes of the PolyData. For this example, StoreTransaction() is called on the returned Poly Data object to store any ProductX transaction. When doing so, all components from the Poly Model objects are used in this scenario. Going further into the StoreTransaction call, you will see the Oracle version of StoreTransaction() (Listing 6.17).

Listing 6.17 A typical nested save operation used against Oracle.
 public virtual bool StoreTransaction( Packet oTrans)    {       try       {          . . .       bResult = StoreTransactionRecord(oTrans);          if(bResult)          {             . . .             return StoreKeys(oTrans);          } }       return false;    } 

StoreTransaction() calls StoreTransactionRecord (Listing 6.18) from the database-specific Poly Data object returned in the initial call. In StoreTransactionRecord(), an SQL command-specific version of StoreTransactionRecord() is called twice. The first call inserts a record into the DATA_LOG table, and the second actually inserts the record into the DATA table. This is part of the Poly Model pattern and is covered in detail in Chapter 5.

Once the record is stored in both the DATA_LOG and DATA tables, the indexes are stored using the Field Indexer pattern. The StoreKeys method drives the Field Indexer in this case. Each implementation is different, and the Poly Model Field Indexer for ProductX is no exception. To design your own, please review Chapter 5. It is the work that was put into the Field Indexer that will make your query breathe easier and your Poly Model perform as you intended. The Field Indexer pattern should give you a head start in the right direction.

Listing 6.18 The nested save operation using two phases ”one for the index and one for data.
 public override bool StoreTransactionRecord( Packet oTrans)    {       try       {          bResult = StoreTransactionRecord(oTrans, . . .));          if(bResult)          {             if (DBGetTransactionRecord(lTransID) == null)             {                . . .       // insert command             }       else             {                . . .       // update command       }             return StoreTransactionRecord(       oTrans, sUpdateOrInsertCommand);          } 

The code snippet in Listing 6.19 shows the Oracle-specific version of StoreTransactionRecord(). Here, Oracle parameters are used (some are not shown) to bind to an embedded PL/SQL command. This is one means of storing XML schemas using the BLOB data type. Oracle streams and stored procedures could also be used and, in fact, are used in the architecture as well. This particular example uses Microsoft's version of the native Oracle provider located in System.Data.OracleClient. (This can be downloaded from Microsoft for free.) The code for creating the sequence number used for the primary key is also shown for clarity.

Listing 6.19 A full nested Poly Model save operation using ADO.NET against Oracle.
 public override bool StoreTransactionRecord(Packet oTrans, string sSQL)    {       PolyData oPolyData = null;       try       {          . . .          oPolyData = DataFactory.CreateDB(sSQL, CommandType.Text);          OracleParameter oInParm0 = (OracleParameter)       oPolyData.AddDataParameter(. . .);          . . .    long lNextCounterValue = GetNextCounterValue(. . .);          oInParm0.Value = lNextCounterValue;          . . .          OracleParameter oInParm2 = (OracleParameter)    oPolyData.AddDataParameter(. . .);          . . .          oInParm2.Value = oTrans.ProductSchemaID;          OracleParameter oInParm3 = (OracleParameter)    oPolyData.AddDataParameter(. . .,    OracleType.Blob,    ParameterDirection.Input);    string sXML = oPolyData.WriteXml(oTrans.RawData,    XmlWriteMode.IgnoreSchema);          oInParm3.Value = Encoding.ASCII.GetBytes(sXML);       int iRowCount = oPolyData.ExecuteNonQuery();if(iRowCount == 1)             return true;       }       . . .    finally       {          if (oPolyData != null)             oPolyData.Close();       . . . 

Unfortunately I cannot show all of the code I would like to, although I believe I show enough to provide you with the essential details to see how one implements the Poly Model patterns in Chapter 5 in a real-world application. Hopefully, this will be only a starting point for you and will give you firm lift toward building an industrial-strength Poly Model in your application. Good luck and happy Poly Modeling.



.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