Distributed applications are now prolific in the enterprise and more and more users are relying on network connectivity both on site and on the go so they can remain productive anywhere and at any time.
Since network connectivity cannot always be guaranteed, what happens when the network goes down or a network connection is simply unavailable? How can you provide your users with the best connected experience regardless of the state of the network?
This article looks at a line of business application built using .NET 2.0, Windows Communication Foundation, and Visual Studio 2005 that automatically chooses the most suitable connection based on the state of the user’s network connection, providing reliability via message queuing on top of it.
Distributed applications are now prolific in the enterprise, and more and more users are relying on network connectivity both on site and on the go so that they can remain productive anywhere and at any time.
I’ll start off with an overview and step-by-step configuration of a WCF service that exposes multiple bindings and wrap up with a pattern for adding logic to a Windows Forms test harness client that detects available network options and chooses the appropriate WCF binding at run time. Where the network is down or otherwise unavailable, I will provide the user with a reliable, “always on” computing experience using Queued Calls via Microsoft Message Queuing (MSMQ). As you will learn, MSMQ integrates seamlessly with WCF services. When network state is online, I will explore appropriate uses for TCP and HTTP depending on the geographical location of the user.
In addition to queued calls, WCF provides a number of powerful reliability features that are supported out of the box including WS-Reliable Messaging for managing message delivery even across multiple hops, and WS-Atomic Transactions for implementing distributed transactions. In this article, I will focus specifically on Queued Services, which are implemented seamlessly in WCF using Microsoft Message Queue (MSMQ). I will discuss these additional reliability features in future articles.
A Case for Queuing
In his excellent white paper, “The Rings of the Enterprise” (http://www.objectwatch.com/newsletters/ObjectWatchNewsletter049.htm), Roger Sessions provided a brilliant analogy about how the design of good service-oriented applications is similar to the way Starbucks shops are run. I have since elaborated upon his analogy and used it time and time again as a metaphor to explain messaging communication patterns to developers and clients alike.
As I sit on a plane to Austin, TX having just finished my Starbucks “venti drip, room for cream”; the metaphor is fresh in my head.
Most Starbucks coffee shops have at least one register, a register clerk that handles the financial transactions, and a barista that makes the extravagant coffees that power corporate America today. I am not here to talk about how Starbucks has become an American icon (it has) or its influence on productivity in the workplace (which I believe is a fascinating topic), but what I have noticed is that in the most efficient locations, there is also a runner that is there to help with confections and non ingredient-intensive beverages. I notice things like this because I am constantly applying programming problems to real-life situations since technology is merely a tool for solving real-world problems. As a result, it is no wonder that approaches to solving technical problems mirror real-life objects and interactions for which metaphors have been created. This is why not all problems can be solved with algorithms, and why sometimes, only heauristics will do the trick.
Next time you stand in line waiting to order your coffee-whether it is a “venti drip, room for cream (so I don’t burn a hole in my pants)” or something a bit fancier like a Caramel Macchiato™-consider what is happening in front of you:
In the last item above, something interesting happens. Based on the type of transaction, the items are delivered either in a classic “request-reply” pattern, where the customer waits momentarily for the response (i.e., the easy venti drip, room for cream, and zucchini walnut muffin), or steps aside waiting for the response which will consist of the customer’s name being called when their Caramel Macchiato™ is ready.
The former is a synchronous call where the customer and the clerk block, or wait until the runner grabs the non ingredient-intensive items. Sometimes, the clerk will even grab the items and serve them to you him or herself if the runner is too busy. The latter, however, is asynchronous, or “publish-subscribe”. Once the financial transaction is complete, the clerk “publishes” the order with the crew behind the counter and uses the customer’s name (a delegate) to correlate the notification (callback), hands off the work to either the runner (if baked goods or simple drip coffees are involved) or the barista (spawning new threads), and then is ready to service other calls (customers).
The customer subscribes to the order by giving the clerk his or her name. After the customer gives his or her name, what the customer does during this time is up to them. They can check their voicemail, check their meeting calendar, or prepare their packets of sugar, stirrer, and napkins. Heck, the customer can even use the restroom if needed depending on how many drinks the barista is working on ahead of the customer’s order.
Now, have you ever been in a line at Starbucks that is so long that a nice employee comes out with a pad and a pen and actually starts taking orders before you reach the register? The best managed Starbucks stores do this. Do you know why? I think it is for two reasons. First, there is something to be said for proactive customer service, and when you are in a rush (as I almost always am), there is just something nice about the shared realization that the situation is unacceptable. Getting my order off my chest doesn’t make the situation any more acceptable, but once I do, I can do other things like crack open my Windows Mobile phone to let my meeting organizer know that I am running late (again).
Second, there is also something more pragmatic about this approach. It is good business. You don’t need a degree in Sociology or Psychology to sense the overwhelming anxiety that is prevalent in those occasional obscenely long lines, nor do you need an MBA to recognize that given the length of the line, losing the business of paying customers is a real possibility. People have places to go and people to see and, in that situation, Starbucks may not feel really confident that they will get each customer’s order in before the customer reluctantly chooses to leave the store. In an effort to fulfill as many of the orders as possible given the long, volatile line, the nice employee cheerfully greets you and takes your order. You might not have your order fulfilled any faster, but you now have an assurance of your order being delivered. You also don’t know if your credit card is going to be authorized or if the items you placed in your order are even available, but your message (written on the employee’s note pad) is guaranteed to (eventually) reach the register.
In the example above, the long line represents the volatility of a network connection between endpoints. When the network is up and running, calls are being serviced efficiently, but sometimes, things go wrong. When they do, you must have a way to gracefully handle the condition. One way to manage the situation is to inform the customer that the call will be queued, which is essentially what the Starbucks employee is doing when he or she takes your order before you get to the register. Again, this is not an ideal situation, but it is far better than telling the customer that they are flat out of luck (or much worse, throwing an exception without any way of picking up the pieces).
Given this context, it is quite fun to pay closer attention to the interactions going on around you the next time you are at your local Starbucks. For example, I often like to analyze how adding another register (multiple instances as opposed to a single instance) or adding additional runners or baristas (threads) might impact the current experience. I’d be interested in hearing your perspectives too!
In this article, I will provide a practical example of how to build services in Windows Communication Foundation (WCF) that support queued calls. I will not cover documentation in depth nor rehash technical details that are already covered extensively on MSDN and in books that are dedicated to WCF (see “Recommended Reading” in the sidebar).
To this end, I assume that you have a foundational understanding of WCF, including Service Contracts, Operation Contracts, Data Contracts, and basic binding fundamentals. If you are new to WCF, I highly recommend you start with a resource such as http://www.code-magazine.com/Article.aspx?quickid=0605051 which will give you a kick start for the rest of this article.
The Traveling Book Salesman (or Woman)
My wife loves to read fiction. Although her reading pace has slowed down significantly since we had children, her idea of fun is still to go out to the used bookstores and bookfairs to feed her reading habit. In fact, we have shelves and shelves full of paperback novels that she hasn’t even touched yet. Some get read, some get left on the shelf, and others are recycled back to one of these bargain booksellers. I am always amazed at what a community there is around the buy/sell/trade paperback book market. For example, a popular novel by a heavy hitter like James Patterson or Tom Clancy might get you two moderately popular books in trade, or you may get $3 towards the purchase of a new or used book. A not-so-popular book may get you 25 cents if anything at all. These stores always seem to be doing good business and, if you’ve ever been to a bargain book fair, you know how crazy the crowds can be. People love books and people love a good bargain.
You should strive to create your artifacts in a manner that is intuitive and reflects the solution architecture.
One of the things I’ve found particularly interesting (ok, it annoys the daylights out of me) is that very few of these bargain book stores have their books inventoried. If you are looking for The Bourne Identity by Robert Ludlum, you have to walk over to the Fiction shelf and search by last name. This is so 20th century!
Let’s build a system that streamlines retail and inventory operations by connecting the sales process with an inventory database using .NET 2.0, WCF, MSMQ, and SQL Server 2005.
I’ve created a simple service-oriented application called “LibroLogic” that streamlines retail and inventory operations by connecting the sales process with an inventory database using .NET 2.0, WCF, MSMQ, and SQL Server 2005.
When a customer comes in to find a book, LibroLogic will do a search against the LibroLogic Database via the LibroLogic Service and give the customer a definitive answer as to whether the book exists somewhere in the store. Better yet, when a customer comes in to sell or trade a book, LibroLogic will be able to determine how many books by that title (if any) are already on hand, allowing the sales associate to base the trade-in value on this simple piece of business intelligence that will be provided by the LibroLogic Service, a WCF service that supports the LibroLogic application. Since this type of search will happen on site, where the UI, service, and database is deployed, the LibroLogic application will communicate with the LibroLogic Service using TCP. For intranet scenarios, this is the best choice because it is the fastest, most efficient transport protocol available to WCF where reliability is required.
Likewise, when a book is sold on site, the application will use TCP to communicate with the LibroLogic Service.
When a store that uses the software takes inventory on the road to participate in a bookfair, using the LibroLogic software, a clerk can flag a book in inventory as being off site before the book leaves the store. Again, the application will use TCP in this scenario because this action will likely happen when on site.
Any time LibroLogic makes a call to the LibroLogic Service, it will intelligently detect the status of the network to ensure that the sales associate can remain productive regardless of the state of the network. The clerk can remove books that are sold off site from inventory in real time when possible. Like the example of the long line at Starbucks, this situation is far from ideal because it may take several hours or days for the service to update the state of the book in inventory, but it is certainly better than not being able to sell the book at all or depend on a manual update to inventory reflecting that the book has been sold.
To summarize, LibroLogic addresses the following features:
Writing the LibroLogic Service
As mentioned above, the three primary activities that the UI will provide is the ability to look up a book by a unique identifier or keyword, flag a book as being off site, and notify the inventory system of a purchase. These first two activities might take place at the store, but the latter may happen while off site.
The service will be distributed into a layered architecture as shown in Figure 1. In keeping with the SOA tenet of autonomy, the LibroLogic Service (service, assemblies, and database) is a self-contained, single logical (and physical) unit so you can deploy it anywhere on the network with little administrative effort. While you should always strive for this kind of assembly and resource allocation, it is common to reuse Business Components, Business Entities, Data Access Components, and the database, especially in legacy systems or where underlying layers have not been factored optimally. As with all decisions in building out a solution architecture, you must determine the compromises of factoring out the functionality into a more autonomous unit of deployment versus the benefit of doing so, which will often come down to budget and calendar time.
Regardless, you should strive to create your artifacts in a manner that is intuitive and reflects the solution architecture. Figure 2 shows the Visual Studio 2005 solution that is available for download with this article (see sidebar “Solution Download”). Since I’ve named each project consistently, determining what each project/assembly does is simple and intuitive. I will refer to the project names as I move through the solution in this article.
Define the Book Data Contract
The first step is to define the data transfer object (DTO) for describing a book. The Data Contract is important because it will model the business domain-a book in this case-which is the cornerstone of the LibroLogic application. In WCF, Data Contracts are DTOs that are either parameters within an Operation Contract or are used as a return type. Listing 1 represents the Book Data Contract that you will use to represent a book that is inventory.
I’ve packaged the Book Data Contract within the LibroLogic.BusinessEntities class library project and provided it within the solution download. You will notice that references to the System.ServiceModel.dll and System.Runtime.Serialization.dll assemblies are present. These assemblies are core to the .NET Framework 3.0 Runtime Components, and specifically, WCF.
The role of the Book Data Contract is simple. It describes book information and tells the DataContractSerializer how to handle the serialization from memory representations of Book into wire format using SOAP and XML. In fact, if you run the following code, you can see what the XML will look like as it is sent across the wire (Figure 3):
LibroLogic.BusinessEntities.Book book = new
LibroLogic.BusinessEntities.Book();
using (System.Xml.XmlTextWriter writer = new
System.Xml.XmlTextWriter(Console.Out))
{
writer.Formatting =
System.Xml.Formatting.Indented;
System.Runtime.Serialization.
DataContractSerializer serializer = new
System.Runtime.Serialization.
DataContractSerializer(
typeof(LibroLogic.
BusinessEntities.Book));
serializer.WriteObject(writer, book);
Console.WriteLine();
}
Define the IInventoryOpsService Contract
With the Book Data Contract in place, I’ve defined the Service Contract for inventory-related activities supported by the LibroLogic Service. Visual Studio stores the Service Contracts for the LibroLogic Service in the LibroLogic.Interfaces project. This contract will support the first and third user features as previously defined. The Service Contract is called IInventoryOperations and consists of three methods as shown below:
[ServiceContract(Namespace=
"http://LibroLogic.com/Services/
InventoryOperations/09/2007")]
public interface IInventoryOps
{
[OperationContract()]
Book GetBook(string uniqueIdentifier);
[OperationContract()]
Book[] GetBooksByTitleKeyWord(string
title);
}
Define the IRetailOps Service Contract
I defined the IRetailOps Service Contract to support the second and fourth user feature, the latter of which deals with the ability to manage a retail sale in a disconnected manner. I designed the IRetailOps Service Contract to support an endpoint powered by a WsHttpBinding, NetTcpBinding, or NetMsmqBinding binding. Specifically, notice that I have set the IsOneWay property on the PurchaseBook Operation Contract:
[ServiceContract(Namespace="http://LibroLogic.com/
Services/RetailOps/9/2007")]
public interface IRetailOps
{
[OperationContract(IsOneWay = true)]
void PurchaseBook(string storeId, string
orderNumber, int quantity, PaymentType
paymentType, Book book);
}
This distinction is important because the NetMsmqBinding binding that powers the Service Contracts must consist of operations that you mark as one-way. The reason for this is that no logic is tied inherently to the endpoint. When you dispatch a message to a queue, you are merely sending it on its way to its ultimate destination. It may reach the destination immediately, after an elapsed period of time, or not at all. As a result, the call is treated as one-way or publish/subscribe. When this requirement is not met, the Service Model throws an InvalidOperationException exception, faulting the channel.
Implementing the IInventoryOps and IRetailOps ServiceContracts
With the Service Contracts for inventory and retail related activities defined, I have implemented the contracts in the LibroLogicService. You will find this class in the LibroLogic.Services project.
As you can see in Listing 2, the implementation is fairly straightforward. In accordance with the solution architecture depicted in Figure 1, the implementation calls the appropriate methods on the InventoryManager and RetailManager components within the LibroLogic.BusinessComponents assembly. Each business component accordingly channels that call to the corresponding methods within the BookDA data access component located in the LibroLogic.DataAccess assembly. While not the focus of this article, I’ve provided the code for the RetailManager and InventoryManager business components in Listing 3 and Listing 4. I also wrote a data access component called “BookDA” (Listing 5). You will notice references to Microsoft.Practices.EnterpriseLibrary.Data, which is a namespace for an assembly that ships with Microsoft Enterprise Library. This namespace contains several ADO.NET helper classes that saved me a lot of time. Speaking of time savers, I also used CodeSmith to generate all the custom stored procedures I wrote (er, generated) for this article. You can find more information about each in the sidebars titled accordingly.
As with all WCF services, notice the fact that the underlying assemblies are part of a service, which has no bearing on the implementation of the service whatsoever. These “Little SOA” implementation details would likely resemble similar characteristics if you were writing a purely component-oriented application. As a matter of fact, much of the same best practices behind or within a service still apply. This distinction is important because “Little SOA enables Big SOA” (Bustamante). That is, without services, you can’t have a service-oriented architecture. There are different aspects and skillsets that apply to Little SOA as opposed to Big SOA. The “Big SOA” aspects are most often in context whenever someone talks about Service-Oriented Architecture in general, because it usually involves the integration of disparate service-oriented applications. That said, the ability to think conceptually in terms of both “Big SOA” and “Little SOA” will give you a tremendous advantage in designing and implementing service-oriented applications that are sustainable.
Configuring and Deploying the LibroLogic Service
As you have seen, the implementation of the LibroLogic Service has been fairly straightforward. The real magic will happen within the configuration, which is one of the most powerful aspects of WCF. The decoupling of configuration and hosting from service implementation in WCF allows for tremendous flexibility in the design and implementation of your service-oriented applications, allowing for a separation of concerns among architects and developers. It cannot be stressed enough that WCF is all about productivity and is not merely a sexy new technology for writing SOA systems and service-oriented applications. If you are a .NET developer, it is the way to write enterprise .NET applications today, period. As a side note, consider Figure 4, which plots the evolution of software engineering. At the simplest level, software engineering consists of the gradual, yet continuous decoupling of constituent artifacts, be it programs, objects, components, or services.
In WCF, you can configure services either programmatically or declaratively. For ease of configuration and to support changes in hosting environments, specifying binding configuration within an application configuration file is a great choice.
The ability to think conceptually in terms of both “Big SOA” and “Little SOA” will give you a tremendous advantage in designing and implementing service-oriented applications that are sustainable and ready for change.
As seen in Listing 6, within the Services element, I have created an entry for the LibroLogic Service:
<service name=
"LibroLogic.Services.LibroLogicService"
behaviorConfiguration="LibroLogicServiceBehavior">
This entry allows you to associate one or more service endpoints with the service. As you might guess, you are going to expose a number of endpoints for each contract specifically designed for a specific transport so that at run time, the client can choose the best endpoint based on the status of the network.
Choosing Appropriate Bindings
According to the user features discussed at length, the LibroLogic Service must be available to the user in both online and offline mode. When online, the client should use the best transport protocol and support all operations provided by the LibroLogic Service. However, when the LibroLogic Service is offline, or the user loses network connectivity, the user should still be able to purchase a book using LibroLogic UI. Specifically, in the event that no network connection is available, the client should queue a purchase message so that when network connectivity is restored, the service can process the purchase message.
Table 1 provides some characteristics of the WsHttpBinding, NetTcpBinding, and NetMsmqBinding bindings that WCF provides right out of the box, which will help you understand which binding is most appropriate for the given scenario addressed in this article. In addition, please see the sidebar for a link to a complete list of bindings and corresponding features supported out of the box.
Configuring the InventoryHTTP Endpoint
As you can see, WsHttpBinding is the only interoperable binding of those listed. In a situation where network connectivity is available via the Internet, the LibroLogic UI should be able to connect to the store to communicate with the LibroLogic Service to query inventory or submit a purchase. In order to do this, messages must be able to traverse firewalls and routers along the way. For this use case, WsHttpBinding is a perfect choice because it not only offers interoperability, but thanks to WCF’s built-in support for WS-Reliable Messaging, it also offers reliability via a new standard pending ratification that provides reliable messaging despite the use of HTTP at the transport level. This is an important feature that is beyond the reach of vanilla Web Services today and will be the topic of a future article. For now, I have added an endpoint to handle requests over the HTTP protocol that uses the binding:
<endpoint
name="InventoryHTTP"
address="http://localhost/LibroLogicService"
binding="basicHttpBinding"
contract="LibroLogic.Interfaces.IInventoryOps"
bindingConfiguration=""/>
Configuring the InventoryTCP Endpoint
When working on site, there is no better choice than NetTcpBinding because it is fast, efficient, and inherently reliable since it uses the TCP at the transport level. Along with WS-Reliable Messaging, I will cover TCP in more detail in an upcoming article. For the time being, I’ll expose an endpoint called InventoryTCP using the NetTcpBinding binding for use on site, where network connectivity is guaranteed (for the most part):
<endpoint
name="InventoryTCP"
adress="net.tcp://localhost/LibroLogicService"
binding="netTcpBinding"
contract="LibroLogic.Interfaces.IInventoryOps"
bindingConfiguration=""/>
Configuring the RetailTCP and RetailHTTP Endpoints
In addition to supporting inventory-related operations such as querying for a particular book or flagging the book with a hold status, the goal of the service is to also support the ability to process retail-related transactions such as purchasing a book when the client is both on site and off site. In off-site scenarios, provided connectivity to the Internet is available, the client should use HTTP. For use on site, the client should use an endpoint called RetailTCP that is exposed using the NetTcpBinding binding. When off site, and provided a connection to the Internet is available, the client exposes an endpoint called RetailHTTP which is powered by the WsHttpBinding to satisfy this requirement:
<endpoint
name="RetailTCP"
address="net.tcp://localhost/
LibroLogicService"
binding="netTcpBinding"
contract="LibroLogic.Interfaces.IRetailOps"
bindingConfiguration=""/>
<endpoint
name="RetailHTTP"
address="http://localhost/LibroLogicService"
binding="basicHttpBinding"
contract="LibroLogic.Interfaces.IRetailOps"
bindingConfiguration=""/>
Configuring the RetailMSMQ Endpoint
The most interesting endpoint, at least for this article, is the RetailMSMQ endpoint. As you might imagine, the client will use this endpoint when the network is unavailable so that the user can still submit a purchase message to the LibroLogic Service.
There are additional features and benefits provided by using an MSMQ-powered endpoint including:
-
Delivery Assurance: Messages sent to a NetMsmqBinding-powered endpoint are guaranteed to be delivered, even when the network or service/receiver is offline.
-
Ordered Delivery: In some scenarios, maintaining the order of messages is important. For example, a business process is typically broken out into several discreet messages that are correlated at the receiver or more simply, the messages may need to be ordered in a way that describes a timeline or sequence of events.
-
Durable Messaging: Once a queue receives a message, the message is persisted to disk and is able to survive machine restarts.
-
Transacted Delivery: When working with transactional queues, a transaction can flow from the client all the way to the final receiver. MSMQ will automatically enlist itself as the root of a transaction and will not remove a message from its queue until it is either delivered to the ultimate receiver, and/or the transaction at the receiver successfully commits.
-
Inherent Reliability: In addition to the reliability benefits provided by delivery assurance, durable messaging, and transacted delivery, you can configure MSMQ for a set retry interval. In addition, you can apply rules for how MSMQ should handle repeated failed delivery attempts.
There are certainly trade-offs to using the features above, and some thought does have to go into how to balance these offerings with throughput and performance requirements. However, when delivery assurance and transacted delivery is a must, leveraging MSMQ is a great choice.
While there is additional configuration required to support the MSMQ features just discussed (which I will review shortly), the basic endpoint configuration is very straightforward:
<endpoint
name="RetailMSMQ"
address="net.msmq://localhost/
private/LibroLogicServiceQueue"
binding="netMsmqBinding"
contract="LibroLogic.Interfaces.IRetailOps"
bindingConfiguration="RetailMSMQBinding"/>
When a client invokes an operation on a service that the service exposes via a NetMsmqBinding-powered endpoint, an interesting thing happens. If the queue is remote (which in most cases, it almost always will be), and is available, depending on the additional binding configuration, WCF sends the message to the receiver queue. However, if the remote queue is unavailable, either because the Service Host was unable to receive requests or the network is down, WCF will automatically create a local queue on the client machine and write the message to queue. Then, based on retry criteria (which you can specify in configuration), the local queue will automatically try to re-send the message.
In keeping with the theme of productivity, WCF makes the configuration of these details extremely simple by exposing several additional binding features.
Enabling Metadata Exchange
If you have been working with WCF for any period of time, you know that WCF takes the concept of explicit boundaries well, quite explicitly. Just as Data Contracts and Data Members along with Service Contracts and Operation Contracts are strictly opt-in, so too is the decision to expose metadata via a WSDL document. This is important because for clients to consume a service, they must be able to learn about what the service offers so that they can create the corresponding proxy types.
There are a couple of options for doing this, but a straightforward approach is to simply add a Service Behavior that instructs the Service Host that it is OK to provide a WSDL document when requested. We could certainly expose this metadata via multiple transport-specific endpoints, but one way to guarantee that all clients can learn about the service is to use an HTTP-based address as shown below:
<behaviors>
<serviceBehaviors>
<behavior name=
"LibroLogicServiceBehavior">
<serviceMetadata httpGetEnabled="true"
httpGetUrl="http://localhost
/LibroLogicServiceMex" />
</behavior>
</serviceBehaviors>
</behaviors>
You will notice that I’ve given the Service Behavior a name. Make sure that you remember to associate the name with the service via the behaviorConfiguration attribute on the service element. I get several questions on my blog and see similar questions on MSDN and other message boards where this simple oversight is the cause of a particular issue.
Installing and Configuring MSMQ
To proceed, you must have MSMQ installed on the machine in which you will host the service. For more information on installing MSMQ, please see the sidebar titled “Installing and Configuring MSMQ”.
Note that for this scenario, the sample code will use a private queue. This is the simplest way to demonstrate queued services in action and closely mimics the behavior of a WCF client that is interacting with a remote queue. It is important to note, however, that in a production scenario, it is very likely that the machine hosting the service would provide a public queue to which the private queue (installed on the client) would transmit messages in a transactional manner. Again, for the purposes of this example, both client and service will run on the same machine and will use a private queue.
Self Hosting
Windows Activation Services (WAS), which ships today in Windows Vista and will ship as part of Windows Server 2008, will provide a first-class WCF hosting environment within IIS. While IIS is a perfectly suitable WCF host, it only supports HTTP-based binding. As such, it is not an option for this service, since the service needs to be able to support TCP, MSMQ, and HTTP transport protocols.
When working on site, there is no better choice than NetTcpBinding because it is fast, efficient, and inherently reliable since it uses the TCP at the transport level.
Since in WCF, any CLR host can host a service including Windows Services, Console Application, and Windows Forms, each of these options support all current bindings and are not limited to HTTP.
When you require a binding other than HTTP for a production-caliber scenario, Windows Services are the best option because they offer identity management and control of process startup and shutdown. For the purposes of this article, I will use a Console Application to demonstrate self hosting.
The code to host a WCF service is very simple and is shown in Listing 7. When you instantiate ServiceHost, the Service Model will inspect the app.config file for the required configuration if it is not provided programmatically.
If you right-click the LibroLogic.Host project and click “Debug” you should see a Console Application screen as shown in Figure 5.
This is a perfect time to test the ability to discover the LibroLogic service as you are almost ready to generate the proxy necessary to consume the service from your test harness. Since you created an HTTP-based metadata exchange address, enter it in your browser as shown in Figure 6. If your browser renders the WSDL document as shown, you are ready to consume the service!
Exploring the LibroLogic Test Harness
I am a big fan of using lightweight test harnesses to demonstrate service functionality. Test harnesses are very useful in communicating functionality and demonstrating progress for services which are inherently abstract to the typical business user. I typically build them using Windows Forms because I am able to have some fun with the test harness by putting some bells and whistles on it that will be visually appealing to the customer. In addition, getting into the habit of creating lightweight test harnesses for your services means that you have a smoke tester for exercising your services. Having test harnesses available for each service that support a business process can be a godsend when troubleshooting a problem within a business process that is distributed across multiple services behind a service boundary. If your business process is made up of half a dozen services and you want to know where the problem lies, you can exercise each service using its respective test harness, isolate the point of failure, and begin a very focused and effective debugging and troubleshooting session (or, better yet, prove that your service is working!).
The user interface for working with the service is very basic, as it is takes a minimalist approach to showing the high-level functionality that the service provides. This said, if there are any WPF gurus out there that want to do a mash-up, shoot me an e-mail.
In exploring the test harness, Figure 7 shows the application with the “Search” tab selected. The search feature allows the user to enter either a unique identifier for the book (commonly a UPC code or ISBN number, however in this case I will use the ID used by the Pubs database) or a title keyword. You will also notice in Figure 7 that a status bar is present that indicates the status of the test harness. Certainly, there are several additional features that you could add to the application, but the sole purpose of this client is to demonstrate the core functionality that addresses the features previously discussed.
The “Purchase” tab allows a user to enter an ID for a given book and select a payment method. Again, this is a minimalist approach to demonstrating the core functionality of the service. Therefore, this form consists of a text box for the ID and a few check boxes to indicate the payment method. You will notice that I’ve labeled one of the check boxes “Flag”. This check box allows the user to indicate that the book has not been sold, but is being flagged to indicate that the book has left the store despite not (yet) being sold. Figure 8 demonstrates the “Purchase” screen.
One other important point of interest is the status bar shown in Figure 9. This control will be visible regardless of the tab that is active and its sole purpose is to indicate whether a network connection is available, and if so, whether the application is running on site or off site.
Detecting the Status of the Network at Run Time
There are a number of decisions that need to be made before you call the WCF proxy. Figure 10 presents a matrix for determining which endpoint you should use given a network and location condition. For instance, if the network is online and the application is running on site, you should use the InventoryTCP endpoint to service search requests. However, if the application is running off site and a network connection is present-assuming the user can access the Internet-you should use the InventoryHTTP endpoint to service the same search requests.
So, the first thing you must do before servicing a search request is figure out if a network connection is present. There are a number of ways to go about this, but it turns out that the .NET Framework 2.0 introduces a new namespace called System.Net.NetworkInformation that includes a class called NetworkInterface that provides just this kind of information. In fact, if you want to query whether the primary network interface is available, you can simply call a method called GetIsNetworkAvailable as shown below:
NetworkInterface.GetIsNetworkAvailable();
The GetIsNetworkAvailable method returns a Boolean value to indicate if the network interface is online and functional. If you have more than one network connection, you can simply call the GetAllNetworkInterfaces method, which will return an array of NetworkInterface objects that can be further interrogated as needed. To do so, simply call the GetIPProperties method on the NetworkInterface class. The GetIPProperties method will return an object that implements IPInterfaceProperties. This interface provides nifty information such as IP, DNS, and Gateway addresses-just the information you would need to determine if the application is being run on site or remotely.
And just when you thought you could take no more, the System.Net.NetworkInformation namespace also provides a class called NetworkChange, which supplies an event called NetworkAvailabilityChanged that fires when the state of the network changes (i.e., a network connection is lost). NetworkChange also provides an event called NetworkAddressChanged that fires when an IP address changes. With either event, the runtime checks a list of subscribers and notifies the configured delegate just as with any .NET event. In this case, your test harness will use delegates to intelligently act upon this information as it happens.
I was so impressed when my mentor and friend, Juval Lowy, showed me this technique that I resolved to write about it ever since. As you (hopefully) are starting to see, at your fingertips lies a tremendously powerful API that when combined with WCF can allow you to develop applications with intelligence that just a few years ago would have been significantly more complex to implement.
Listing 8 shows the code for the test harness. One of the first things you will notice is two properties, Online and OnSite, both of which are thread-safe. The reason for this is that the NetworkAvailabilityChanged and NetworkAddressChanged events will be calling their corresponding delegates using threads from the thread pool. As such, support for multithreaded access is a must.
The Online property simply returns true or false to indicate whether the application has a network connection:
public bool Online
{
get
{
lock (this)
{
return m_Online;
}
}
set
{
lock (this)
{
m_Online = value;
}
}
}
The OnSite property works the same way, signaling if the location of the user is other than the configured location that is considered on site:
public bool OnSite
{ get
{
lock (this)
{
return m_Onsite;
}
}
set
{
lock (this)
{
m_Onsite = value;
}
}
}
Within the constructor of the LibroLogicClient Windows Forms class, you will notice the following code that sets the value of the Online and OnSite properties, subscribes to the events of interest, and provides and initializes the status bar using the appropriate methods:
public LibroLogicClient()
{
InitializeComponent();
NetworkChange.
NetworkAvailabilityChanged +=
OnNetworkAvailabilityChanged;
NetworkChange.NetworkAddressChanged +=
OnNetworkAddressChanged;
bool online = false;
bool onsite = false;
online = NetworkInterface.
GetIsNetworkAvailable();
onsite = QueryLocation();
UpdateNetworkAvailableStatus(online);
UpdateNetworkLocationStatus(onsite);
}
The UpdateNetworkAvailableStatus method simply sets the Online property and subsequently calls a method called UpdateStatusBar:
void UpdateNetworkAvailableStatus(bool online)
{
Online = online;
UpdateStatusBar();
}
The UpdateStatusBar method updates the text on the m_StatusBar control (which is rendered at the bottom of the LibroLogic UI as shown in Figure 7 and Figure 8). You must synchronize this update with the UI thread since the events will be firing from the thread pool. The MethodInvoker method handles the synchronization and updates the status bar with the respective string value:
private void UpdateStatusBar()
{
if (m_StatusBar.InvokeRequired)
{
m_StatusBar.Invoke(new
MethodInvoker(
UpdateStatusBar));
}
else
{
m_StatusBar.Text = "Test";
string status = Online ? "Current
Status: Online" :
"Current Status: Offline";
string locStatus = OnSite ? " |
Onsite" : " | Offsite";
m_StatusBar.Text = status +
locStatus;
}
}
You will recall that in order to support the ability to detect intermittent network status at run time, you must subscribe to the NetworkAvailabilityChanged and NetworkLocationChanged events. I have named the handlers for these events OnNetworkAvailabilityChanged and OnNetworkAddressChanged respectively.
The OnNetworkAvailabilityChanged method is of the NetworkAvailabilityChangedEventHandler delegate type and calls the UpdateNetworkAvailableStatus method just discussed:
void OnNetworkAvailabilityChanged(object sender,
NetworkAvailabilityEventArgs args)
{
UpdateNetworkAvailableStatus(
args.IsAvailable);
}
The OnNetworkAddressChanged method is of the delegate type NetworkAddressChangedEventHandler. Any time you call this handler, it will simply query the IP information using the QueryLocation method and update the status bar in much the same way as the UpdateNetworkAvailableStatus method. You can see all of this code in Listing 8:
void OnNetworkAddressChanged(object sendes,
EventArgs args)
{
UpdateNetworkLocationStatus(
QueryLocation());
}
Implementing the LibroLogic Service Gateway
A common pattern for integrating services into existing applications is the Gateway pattern. Martin Fowler describes this pattern in detail in his excellent book, Patterns for Enterprise Architecture (see sidebar: “Gateway Pattern”) and it is also covered well in the Microsoft Patterns & Practices paper titled, “Enterprise Solution Patterns using Microsoft .NET”. A Service Gateway is a wrapper around the component or service that abstracts the details of the call to a service (or in-process component for that matter). In addition to providing location transparency to the consumer, a gateway allows for the consumer of a service to be insulated from the transport details and service types that the service boundary exposes. This is always a good idea because service details will inevitably change, as will potential service providers, so it is always good to protect your business layer (and thus the application itself) by decoupling it from these details.
Test harnesses are very useful in communicating functionality and demonstrating progress for services which are inherently abstract to the typical business user.
In this scenario, the Service Gateway pattern will be particularly useful for insulating the details of which endpoint you are calling from the presentation code.
The LibroLogicServiceGateway class is located in the LibroLogic.UI.ServiceGateways project. The definition of the methods supported by the ILibroLogicServiceGateway gateway is shown in Listing 9. Listing 10 shows the implementation for the current service provider(s).
You will notice that the methods included in the LibroLogicServiceGateway are very similar to those defined in the IInventoryOps and IRetailOps Service Contracts. This is the case because in this scenario, we have control of both the Big SOA and Little SOA aspects of the LibroLogic application. However, in cases when you are integrating a service from a vendor or are integrating an application or service that for political or other reasons is beyond your control, it is likely that the method names you come up with for your service gateway will differ, if not for the simple fact that the operations provided by the service may not be all that intuitive to your own domain model. In addition, should the service provider ever be replaced, any clients behind the ILibroLogicServiceGateway interface will be completely shielded from change.
Because I designed both sides of the wire, the only difference between the method signature of the operation at the service and the members defined in the gateway interface is the addition of two Boolean parameters: online and onsite:
public interface ILibroLogicServiceGateway
{
Book GetBook(string uniqueIdentifier, bool
online, bool onsite);
Book[] GetBooksByTitleKeyWord(string
title, bool online, bool onsite);
void PurchaseBook(string storeId, string
orderNumber, int quantity, PaymentType
paymentType, Book book, bool online,\ bool
onsite);
}
You could easily argue that these parameters are a design smell and that the network status detection code has no business in the presentation tier, or at least not in the Windows Forms code-behind. I wholeheartedly agree! However, keep in mind that this is merely a test harness and I wanted to keep the user interface as simple as possible while illustrating the use of the System.Net.NetworkInformation API without getting too far away from the service, which is the focus of this article. As they used to tell us in the Army: “This is for illustration and illustration purposes only.”
The LibroLogicServiceGateway simply implements the ILibroLogicServiceGateway interface. As long as clients bind to the ILibroLogicServiceGateway, they will have a binary shield protecting them from changes beyond the service boundary.
Here is the implementation of PurchaseBook. As seen in Listing 10, you will use the method to handle both the purchase of a book and the flagging of a book when it is being moved off site. The decision as to which endpoint to use for the proxy instantiation is based on the value of the online and onsite parameters that are passed in from the LibroLogicClient instance. The ability to dynamically choose which endpoint (transport) to use is facilitated by the RetailOpsClient constructor overload which takes the name of the endpoint to use when preparing the communication channel:
public void PurchaseBook(string storeId, string
orderNumber, int quantity, string paymentTerms,
Book book, bool online, bool onsite)
{
LibroLogic.UI.ServiceGateways.
localhost.RetailOpsClient proxy;
if (online & onsite)
{
proxy = new LibroLogic.UI.
ServiceGateways.localhost.
RetailOpsClient("RetailTCP");
}
else if (online & offiste)
{
proxy = new LibroLogic.UI.
ServiceGateways.localhost.
RetailOpsClient("RetailHTTP");
}
else
{
proxy = new LibroLogic.UI.
ServiceGateways.localhost.
RetailOpsClient("RetailMSMQ");
}
}
Another important detail to understand is that there are two representations of the Book class. Earlier, I walked you through the Book Data Contract, which is defined within the context of the service and supports SOAP serialization of the properties that have been opted in. However, keep in mind that you use the Book Data Contract for interoperability purposes only and it has no business bleeding into the local application’s business domain. So, in essence, you have two classes called Book that may look the same, but are completely different due to their namespaces. An added benefit of the Service Gateway pattern is that it serves to address this very common transformation problem. As subtle as it might be, it is important to note that the Book parameter that is included in the methods provided by the LibroLogicServiceGateway is of type LibroLogic.UI.BusinessEntities, not http://LibroLogic.com/services/09/2007. Listing 10 contains two additional methods that are not defined in the interface: MapLocalBookToProxy and MapProxyBookToLocal that handle this translation. Contending with this subtlety is important if you are truly building service-oriented applications. The temptation to distribute service assemblies containing common types to the client is a hangover from the pre-SOA techniques and you should avoid it. Furthermore, allowing types behind the service boundary to bleed into the consuming application’s business domain is a recipe for integration spaghetti which you should also avoid. Making a habit of using the Gateway pattern will help insulate you from both.
Implementing the Search and Purchase Event Handlers
When a user enters an ID to search for a particular book, the test harness calls the btnSearchById_Click method to handle the Click event.
Likewise, when the user enters a title to find books that match that search string, the test harness invokes the btnSearchByTitle_Click event handler.
The purpose of the event handlers is to wire the Click event to the functionality that is responsible for handling the event. Recall that the IRetailOps Service Contract defines two methods that are designed specifically to handle these kinds of requests:
[OperationContract(IsOneWay = true)]
void PurchaseBook(string storeId, string
orderNumber, int quantity, PaymentType
paymentType, Book book);
Instead of instantiating the proxy and calling these operations directly, the event handlers simply channel the call to the appropriate method on the LibroLogicServiceGateway. The following code demonstrates the invocation of the PurchaseBook method on the gateway within the btnSubmitPurchase_Click method (the complete code for the test harness is provided in Listing 8):
ILibroLogicServiceGateway gateway = new
LibroLogicServiceGateway() as
ILibroLogicServiceGateway;
gateway.PurchaseBook(storeNumber,
orderNumber, quantity, paymentType,
book, Online, OnSite)
Each event handler works the same way. The beauty of this design is that details of which endpoint to call are all abstracted from the user interface, making the code consistent, readable, and easy to understand. The user interface has no idea what endpoint is being used or that the test harness may queue this particular call.
Surviving Transient Network Conditions
With the focus on this article on queued services, let’s exercise the ability to queue messages when the network is offline. With the LibroLogic application up and running (along with the LibroLogic Service, of course), test the ability to query for books using both supported methods. For example, when you enter a search term, such as “computers”, you should get a list of books that match that criteria as shown in Figure 7. Make a note of the TitleId for one of the books. The TitleId serves as the unique identifier for the book, which in most cases would be a UPC code or ISBN number.
A gateway allows for the consumer of a service to be insulated from the transport details and service types that the service boundary exposes.
Click the “Purchase” tab and enter the TitleId from the book in the results of the grid on the Search tab. Enter a quantity of 1 and select the “Credit Card” check box. Click the “Submit Purchase” button. A message should appear in the Messages section indicating that the request was successful as shown in Figure 11. In this case, the RetailTCP endpoint was used because the status indicates that the application is both online and on site.
Note the status of the icon in your task bar that represents your active network connection and makes sure it is online. Now, with the LibroLogic test harness running, go to My Network Places, right-click, and then click Properties. Find your network connection and click “Disable” as shown in Figure 12. You will notice that in a few moments, the status bar on the test harness will indicate that the network is now off line. Your network icon on your task bar, as shown in Figure 13, should confirm this. You will also notice that on the Search tab, the “Search by Title” and “Search by Id” buttons are disabled. This is because these features require a request-reply messaging pattern that is not supported in offline mode. Attempting to use these features in offline mode would result in an exception.
At this point, we are simulating that the network is down. However, since the LibroLogic service is running on the same machine, it will happily pick up messages from the queue. To complete the scenario, close the LibroLogic Service host by exiting the console window.
Now, click the “Purchase” tab and enter a valid TitleId, along with the other information, ensuring that you chose a payment method. Click “Submit Purchase”. As shown in Figure 14, a message appears indicating that the purchase message has been sent! Despite the network being down and the service being completely unreachable (at least at the moment), from the client’s perspective, the ability to purchase a book is still fully supported. While the application is in offline mode, it is still operational, albeit with limited functionality.
Feel free to click the “Submit Purchase” button a few more times, changing the values you entered in the corresponding text boxes (note that Order Number must be unique or it will violate a constraint on the database). The messages representing the purchase request have been queued locally on the client machine. To verify this, open the Computer Management MMC snap-in by right-clicking “My Computer” and clicking “Manage”. Towards the bottom of the tree, you will see a group called “Applications and Services”. Expand this group and expand the group under it called “Message Queuing”. Under “Message Queuing”, expand “Private Queues”. As seen in Figure 15, you will see a queue called “LibroLogicServiceQueue”, and in the Queue Messages folder, you will see a message for each purchase request.
Now let’s simulate the network coming back on line. With the Computer Management MMC snap-in still running (and the MSMQ groups expanded), find the disabled network icon on your task bar. The disabled network icon should resemble something similar to Figure 13. Enable your network card by right-clicking the disabled network icon on your task bar and clicking “Enable” as shown in Figure 16. The status changes back to indicate that the network is online. Again, you will notice that in just a few moments, the status bar on the test harness will change to indicate that the network is online. Although the network is now online, remember that you closed the service host. As a result, it will not process the queued messages until the next time it starts. Start it now. Once you do, for every purchase message in the queue, you should see an entry in the console hosting the service similar to Figure 17. Finally, look in the Queue Messages folder under the LibroLogicServiceQueue and you will notice that the queue is now empty. The messages have been delivered to the LibroLogic Service automatically.
In addition, to verify that the messages have indeed been received, on the SQL Server machine that has been configured for the LibroLogic Service, feel free to open the Pubs database and open the “Sales” table. You will see an entry for each of the purchase requests.
Summary
As you have seen, adding support for queued services to your WCF applications is not very difficult to do and adds increased reliability for your users. Given the elegance of the WCF hosting and configuration architecture, you are able to build robust applications with a primary focus on delivering business value, leaving many of the configuration details (hosting, transport, bindings, etc.) to a deployment-time decision. This is a key strength of WCF that yields significant productivity.
In addition, I provided an overview of the System.Net.NetworkInformation namespace, which provides a number of APIs for discovering network specific information and how you can use this information to determine the appropriate binding to use at run time.
I also showed you how to write a lightweight test harness to exercise the service and I demonstrated an implementation of the Service Gateway design pattern to shield yours clients from the details of the service it is interacting with.
Last but not least, I showed you how to leverage Microsoft Message Queue to enable your services to support queued calls. While the ability to queue messages in a disconnected scenario is a key way to add enhanced reliability to your applications, there are still some gems in WCF that enable you to build even more robust applications that are reliable across various transport protocols and ensure that regardless of the outcome of a message, the underlying system is in a consistent state. I’ll explore more of these features in detail in upcoming articles, so stay tuned!
Listing 1: The Book Data Contract
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Serialization;
namespace LibroLogic.BusinessEntities
{
[DataContract(Namespace="http://LibroLogic.com/
BusinessEntities/Book/09/2007")]
public class Book
{
private string m_Id;
private string m_Title = String.Empty;
private string m_Type = String.Empty;
private string m_PubId = String.Empty;
private decimal m_Price;
private decimal m_Advance;
private int m_royalty;
private int m_YtdSales;
private string m_Notes = String.Empty;
private DateTime m_Pubdate;
private int m_OnHand;
[DataMember()]
public string TitleId
{
get
{
return m_Id;
}
set
{
m_Id = value;
}
}
[DataMember()]
public string Title
{
get
{
return m_Title;
}
set
{
m_Title = value;
}
}
[DataMember()]
public string Type
{
get
{
return m_Type;
}
set
{
m_Type = value;
}
}
[DataMember()]
public string PubId
{
get
{
return m_PubId;
}
set
{
m_PubId = value;
}
}
[DataMember()]
public decimal Price
{
get
{
return m_Price;
}
set
{
m_Price = value;
}
}
[DataMember()]
public decimal Advance
{
get
{
return m_Advance;
}
set
{
m_Advance = value;
}
}
[DataMember()]
public int Royalty
{
get
{
return m_royalty;
}
set
{
m_royalty = value;
}
}
[DataMember()]
public int YtdSales
{
get
{
return m_YtdSales;
}
set
{
m_YtdSales = value;
}
}
[DataMember()]
public string Notes
{
get
{
return m_Notes;
}
set
{
m_Notes = value;
}
}
[DataMember()]
public DateTime Pubdate
{
get
{
return m_Pubdate;
}
set
{
m_Pubdate = value;
}
}
[DataMember()]
public int OnHand
{
get
{
return m_OnHand;
}
set
{
m_OnHand = value;
}
}
}
}
Listing 2: The Implementation of the LibroLogicService class
using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;
using System.Runtime.Serialization;
using LibroLogic.BusinessEntities;
using LibroLogic.BusinessComponents;
using LibroLogic.Interfaces;
namespace LibroLogic.Services
{
[ServiceBehavior(Namespace="http://LibroLogic.com/
Services/LibroLogic/09/2007")]
public class LibroLogicService : IInventoryOps, IRetailOps
{
#region IInventoryOps Members
public Book GetBook(string uniqueIdentifier)
{
Book[] book = new Book[1];
try
{
book = InventoryManager.GetBooks(uniqueIdentifier,
null);
}
catch (Exception ex)
{
Console.WriteLine("An exception was thrown within
the service host. Exception: " + ex.Message);
}
finally
{
// For demo purposes only
Console.WriteLine("Book with title id of " +
book[0].TitleId + " returned.");
}
return book[0];
}
public Book[] GetBooksByTitleKeyWord(string title)
{
Book[] books = new Book[1];
try
{
books = InventoryManager.GetBooks(null, title);
Console.WriteLine(books.Length + " books
returned.");
}
catch (Exception ex)
{
Console.WriteLine("An exception was thrown within
the service host. Exception: " + ex.Message);
}
return books;
}
#endregion
#region IRetailOps Members
public void PurchaseBook(string storeId, string
orderNumber, int quantity, PaymentType paymentType,
Book book)
{
try
{
RetailManager.PurchaseBook(storeId, orderNumber,
quantity, paymentType, book);
Console.WriteLine("Order number " +
orderNumber + " processed.");
}
catch (Exception ex)
{
Console.WriteLine("An exception was thrown
within the service host. Exception: " + \ \
ex.Message);
}
}
#endregion
}
}
Listing 3: The LibroLogic InventoryManager
using System;
using System.Collections.Generic;
using System.Text;
using LibroLogic.BusinessEntities;
using LibroLogic.DataAccess;
namespace LibroLogic.BusinessComponents
{
public class InventoryManager
{
static Book[] books;
static BookDA da = new BookDA("pubs");
public static Book[] GetBooks(string uniqueIdentifier,
string titleKeyWord)
{
if (uniqueIdentifier != null)
{
books = new Book[1];
books[0] = da.GetBookById(uniqueIdentifier);
}
else
{
books = da.GetBooksByTitleKeyWord(
titleKeyWord).ToArray();
}
return books;
}
}
}
Listing 4: The LibroLogic RetailManager
using System;
using System.Collections.Generic;
using System.Text;
using LibroLogic.BusinessEntities;
using LibroLogic.DataAccess;
namespace LibroLogic.BusinessComponents
{
public class RetailManager
{
static BookDA da = new BookDA("pubs");
public static int PurchaseBook(string storeId, string
orderNumber, int quantity, PaymentType paymentType,
Book book)
{
return da.InsertSale(storeId, orderNumber, quantity,
paymentType, book);
}
}
}
Listing 5: The LibroLogic Data Access Layer
using System;
using System.Data;
using System.Data.Common;
using System.Collections.Generic;
using System.Text;
using Microsoft.Practices.EnterpriseLibrary.Data;
using LibroLogic.BusinessEntities;
namespace LibroLogic.DataAccess
{
/// <summary>
/// Data Access component that encapsulates interaction of
///Book information with database.
/// </summary>
public class BookDA
{
private Database m_Database;
/// <summary>
/// Default Constructor
/// </summary>
public BookDA()
{
m_Database = DatabaseFactory.CreateDatabase("Pubs");
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="databaseName">The name of
/// the database in the configuration file.</param>
public BookDA(string databaseName)
{
m_Database = DatabaseFactory.CreateDatabase(
databaseName);
}
/// <summary>
/// Creates a sales activity record of a Book using a
/// provider agnostic database factory.
/// </summary>
/// <param name="storeId">The id for the store where the
/// sale took place.</param>
/// <param name="orderNumber">The order number for the
/// sale.</param>
/// <param name="quantity">The number of books
/// purchased.</param>
/// <param name="paymentTerms">The payment terms of the
/// transaction (either credit card, check or flag to
/// denote that the book is out on the road.</param>
/// <param name="book">An instance of the book that was
/// purchased.</param>
/// <returns>An integer representing an auto-incrementing
/// identifier in the sales table.</returns>
public int InsertSale(string storeId, string orderNumber,
int quantity, PaymentType paymentType,Book book)
{
DbCommand command = m_Database.
GetStoredProcCommand("usp_InsertSale");
m_Database.AddInParameter(command, "stor_id",
DbType.String, storeId);
m_Database.AddInParameter(command, "ord_num",
DbType.String, orderNumber);
m_Database.AddInParameter(command, "ord_date",
DbType.DateTime, System.DateTime.Now);
m_Database.AddInParameter(command, "qty",
DbType.Int32, quantity);
m_Database.AddInParameter(command, "payterms",
DbType.String, paymentType.ToString());
m_Database.AddInParameter(command, "title_id",
DbType.String, book.TitleId);
return (int) m_Database.ExecuteScalar(command);
}
/// <summary>
/// Retrieves a unique Book by the given id.
/// </summary>
/// <param name="id">The id for the given book.</param>
/// <returns>An object of type Book.</returns>
public Book GetBookById(string id)
{
DbCommand command = m_Database.
GetStoredProcCommand("usp_SelectTitleByTitleId");
m_Database.AddInParameter(command, "title_id",
DbType.String, id);
Book book = new Book();
using (IDataReader dataReader =
m_Database.ExecuteReader(command))
{
while (dataReader.Read())
{
book.TitleId =
dataReader["title_id"].ToString();
book.Title = dataReader["title"].ToString();
book.Type = dataReader["type"].ToString();
book.PubId = dataReader["pub_id"].ToString();
book.Price = Decimal.Parse(
dataReader["price"].ToString());
book.Advance = Decimal.Parse(
dataReader["advance"].ToString());
book.Royalty = (int)dataReader["royalty"];
book.YtdSales = (int)dataReader["ytd_sales"];
book.Notes = dataReader["notes"].ToString();
book.Pubdate = (System.DateTime)
dataReader["pubdate"];
book.OnHand = (int)dataReader["on_hand"];
}
}
return book;
}
/// <summary>
/// Retrieves all Book objects that match
/// the search criteria.
/// </summary>
/// <param name="title">The title of the book for
/// which to search.</param>
/// <returns> A collection of objects of type
/// Book</returns>
public List<Book> GetBooksByTitleKeyWord(string title)
{
DbCommand command =
m_Database.GetStoredProcCommand(
"usp_SelectTitlesByTitle");
m_Database.AddInParameter(command, "title",
DbType.String, title);
List<Book> books = new List<Book>();
using (IDataReader dataReader =
m_Database.ExecuteReader(command))
{
while (dataReader.Read())
{
Book book = new Book();
book.TitleId =
dataReader["title_id"].ToString();
book.Title = dataReader["title"].ToString();
book.Type = dataReader["type"].ToString();
book.PubId = dataReader["pub_id"].ToString();
book.Price = dataReader.IsDBNull(
dataReader.GetOrdinal("price")) ? 0.00m :
Decimal.Parse(dataReader["price"].ToString());
book.Advance = dataReader.IsDBNull(
dataReader.GetOrdinal("advance")) ? 0.00m :
Decimal.Parse(dataReader["advance"].
ToString());
book.Royalty = dataReader.IsDBNull(
dataReader.GetOrdinal("royalty")) ? 0 :
(int)dataReader["royalty"];
book.YtdSales = dataReader.IsDBNull(
dataReader.GetOrdinal("ytd_sales")) ? 0 :
(int)dataReader["ytd_sales"];
book.Notes = dataReader.IsDBNull(
dataReader.GetOrdinal("notes")) ?
String.Empty: (string)dataReader["notes"];
book.Pubdate = (System.DateTime)
dataReader["pubdate"];
book.OnHand = (int)dataReader["on_hand"];
books.Add(book);
}
}
return books;
}
}
}
Listing 6: The application configuration file for the LibroLogic Service
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="Pubs" connectionString="Server=.;
Database=pubs;Trusted_Connection=yes;"
providerName="System.Data.SqlClient"/>
</connectionStrings>
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="LibroLogicServiceBehavior">
<serviceMetadata httpGetEnabled="true"
httpGetUrl="http://localhost/
LibroLogicServiceMex" />
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service name="LibroLogic.Services.LibroLogicService"
behaviorConfiguration="LibroLogicServiceBehavior">
<endpoint
name="InventoryTCP"
address="net.tcp://localhost/LibroLogicService"
binding="netTcpBinding"
contract="LibroLogic.Interfaces.IInventoryOps"
bindingConfiguration=""/>
<endpoint
name="InventoryHTTP"
address="http://localhost/LibroLogicService"
binding="basicHttpBinding"
contract="LibroLogic.Interfaces.IInventoryOps"
bindingConfiguration=""/>
<endpoint
name="RetailTCP"
address="net.tcp://localhost/LibroLogicService"
binding="netTcpBinding"
contract="LibroLogic.Interfaces.IRetailOps"
bindingConfiguration=""/>
<endpoint
name="RetailHTTP"
address="http://localhost/LibroLogicService"
binding="basicHttpBinding"
contract="LibroLogic.Interfaces.IRetailOps"
bindingConfiguration=""/>
<endpoint
name="RetailMSMQ"
address="net.msmq://localhost/private/
LibroLogicServiceQueue"
binding="netMsmqBinding"
contract="LibroLogic.Interfaces.IRetailOps"
bindingConfiguration="RetailMSMQBinding"/>
</service>
</services>
<bindings>
<netMsmqBinding>
<binding name="RetailMSMQBinding">
<security mode="None">
</security>
</binding>
</netMsmqBinding>
</bindings>
</system.serviceModel>
</configuration>
Listing 7: The ServiceHost hosts the LibroLogic Service
using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;
using System.Messaging;
using LibroLogic.Services;
using LibroLogic.Interfaces;
namespace LibroLogic.Host
{
class Program
{
static void Main(string[] args)
{
try
{
ServiceHost host = new
ServiceHost(typeof(LibroLogicService));
host.Open();
System.Threading.Thread.Sleep(5000);
Console.WriteLine("LibroLogic Service is
Ready...");
Console.ReadLine();
host.Close();
}
catch (Exception ex)
{
Console.WriteLine("Error creating service host: "
+ ex.Message);
}
finally
{
Console.ReadLine();
}
}
}
}
Listing 8: The Test Harness Client
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.ServiceModel;
using System.Messaging;
using System.Net.NetworkInformation;
using LibroLogic.UI.BusinessEntities;
using LibroLogic.UI.ServiceGateways;
namespace LibroLogic.UI
{
public partial class LibroLogicClient : Form
{
bool m_Online = false;
bool m_Onsite = false;
public bool Online
{
get
{
lock (this)
{
return m_Online;
}
}
set
{
lock (this)
{
m_Online = value;
}
}
}
public bool OnSite
{
get
{
lock (this)
{
return m_Onsite;
}
}
set
{
lock (this)
{
m_Onsite = value;
}
}
}
public LibroLogicClient()
{
InitializeComponent();
NetworkChange.NetworkAvailabilityChanged +=
OnNetworkAvailabilityChanged;
NetworkChange.NetworkAddressChanged +=
OnNetworkAddressChanged;
bool online = false;
bool onsite = false;
online = NetworkInterface.GetIsNetworkAvailable();
onsite = QueryLocation();
UpdateNetworkAvailableStatus(online);
UpdateNetworkLocationStatus(onsite);
}
private void btnSubmitPurchase_Click(object sender,
EventArgs e)
{
string storeNumber = txtStore.Text;
string orderNumber = txtOrderNo.Text;
int quantity = Int16.Parse(txtBookQty.Text);
Book book = new Book();
book.TitleId = txtBookId.Text;
PaymentType paymentType;
string paymentMethod = GetPaymentMethod();
switch (paymentMethod)
{
case "Check":
paymentType = PaymentType.Check;
break;
case "Credit":
paymentType = PaymentType.CreditCard;
break;
case "Trade":
paymentType = PaymentType.Trade;
break;
case "Hold":
paymentType = PaymentType.Hold;
break;
default:
paymentType = PaymentType.Hold;
break;
}
ILibroLogicServiceGateway gateway = new
LibroLogicServiceGateway() as
ILibroLogicServiceGateway;
gateway.PurchaseBook(storeNumber, orderNumber,
quantity, paymentType, book, Online, OnSite);
}
private void btnSearchByTitle_Click(object sender,
EventArgs e)
{
string bookKeyWord = txtSearchByTitle.Text;
ILibroLogicServiceGateway gateway = new
LibroLogicServiceGateway() as
ILibroLogicServiceGateway;
Book[] booksFound =
gateway.GetBooksByTitleKeyWord(
bookKeyWord, Online, OnSite);
dgvBooks.DataSource = booksFound;
}
private void btnSearchById_Click(object sender,
EventArgs e)
{
string bookId = txtSearchById.Text;
ILibroLogicServiceGateway gateway = new
LibroLogicServiceGateway() as
ILibroLogicServiceGateway;
Book bookFound = gateway.GetBook(bookId, Online,
OnSite);
dgvBooks.DataSource = bookFound;
}
private string GetPaymentMethod()
{
string paymentMethod = String.Empty;
if (cbCheck.Checked)
{
paymentMethod = "Check";
}
else if (cbCreditCard.Checked)
{
paymentMethod = "Credit";
}
else if (cbTrade.Checked)
{
paymentMethod = "Trade";
}
else
{
paymentMethod = "Hold";
}
return paymentMethod;
}
/// <summary>
/// Updates the Online property based on network
/// availability of the user.
/// </summary>
/// <param name="online">true or false based on
/// network availability signal.</param>
/// <remarks>This method is called on a seperate method
/// than that of the UI thread.</remarks>
void UpdateNetworkAvailableStatus(bool online)
{
Online = online;
UpdateStatusBar();
}
/// <summary>
/// Updates the Onsite property based on network
/// location of the user.
/// </summary>
/// <param name="onsite">true or false based on the
/// location signal.</param>
/// <remarks>This method is called on a seperate method
/// than that of the UI thread.</remarks>
void UpdateNetworkLocationStatus(bool onsite)
{
OnSite = onsite;
UpdateStatusBar();
}
/// <summary>
/// Updates the status bar indicating network
/// availability and location by marshalling the worker
/// thread back to the UI thread. This is necessary to
/// prevent an exception. </summary>
private void UpdateStatusBar()
{
if (m_StatusBar.InvokeRequired)
{
m_StatusBar.Invoke(new
MethodInvoker(UpdateStatusBar));
}
else
{
m_StatusBar.Text = "Test";
string status = Online ? "Current Status:
Online" : "Current Status: Offline";
string locStatus = OnSite ? " | Onsite" : " |
Offsite";
m_StatusBar.Text = status + locStatus;
}
}
void OnNetworkAvailabilityChanged(object sender,
NetworkAvailabilityEventArgs args)
{
UpdateNetworkAvailableStatus(args.IsAvailable);
}
void OnNetworkAddressChanged(object sendes,
EventArgs args)
{
UpdateNetworkLocationStatus(QueryLocation());
}
private static bool QueryLocation()
{
bool onsite = false;
NetworkInterface[] nics =
NetworkInterface.GetAllNetworkInterfaces();
NetworkInterface nic = nics[1];
IPInterfaceProperties props = nic.GetIPProperties();
string currentDnsSuffix = props.DnsSuffix;
string homeDnsSuffix =
ConfigurationManager.AppSettings["HomeDNS"];
int found =
currentDnsSuffix.LastIndexOf(homeDnsSuffix);
if (found > 0)
{
onsite = true;
}
return onsite;
}
}
}
Listing 9: The LibroLogic Service Gateway Interface
using System;
using System.Collections.Generic;
using System.Text;
using LibroLogic.UI.BusinessEntities;
namespace LibroLogic.UI.ServiceGateways
{
public interface ILibroLogicServiceGateway
{
Book GetBook(string uniqueIdentifier,
bool online, bool onsite);
Book[] GetBooksByTitleKeyWord(string title,
bool online, bool onsite);
void PurchaseBook(string storeId,
string orderNumber, int quantity,
PaymentType paymentType, Book book,
bool online, bool onsite);
}
}
Listing 10: The Implementation of the LibroLogic Service Gateway
using System;
using System.Collections.Generic;
using System.Text;
using local = LibroLogic.UI.BusinessEntities;
using service = LibroLogic.UI.ServiceGateways.localhost;
namespace LibroLogic.UI.ServiceGateways
{
public class LibroLogicServiceGateway :
ILibroLogicServiceGateway
{
#region ILibroLogicServiceGateway Members
public local.Book GetBook(string uniqueIdentifier, bool
online, bool onsite)
{
service.InventoryOpsClient proxy = null;
if (online & onsite)
{
proxy = new LibroLogic.UI.
ServiceGateways.localhost.
InventoryOpsClient("InventoryTCP");
}
else if (online & !onsite)
{
proxy = new LibroLogic.UI.
ServiceGateways.localhost.
InventoryOpsClient("InventoryHTTP");
}
service.Book bookProxy =
proxy.GetBook(uniqueIdentifier);
local.Book bookLocal =
MapProxyBookToLocal(bookProxy);
return bookLocal;
}
public local.Book[] GetBooksByTitleKeyWord(string
title, bool online, bool onsite)
{
service.InventoryOpsClient proxy = null;
if (online & onsite)
{
proxy = new LibroLogic.UI.
ServiceGateways.localhost.
InventoryOpsClient("InventoryTCP");
}
else if (online & !onsite)
{
proxy = new LibroLogic.UI.
ServiceGateways.localhost.
InventoryOpsClient("InventoryHTTP");
}
service.Book[] booksProxy =
proxy.GetBooksByTitleKeyWord(title);
local.Book[] booksLocal = new
LibroLogic.UI.BusinessEntities.
Book[booksProxy.Length];
for (int i = 0; i < booksProxy.Length; i++)
{
booksLocal[i] =
MapProxyBookToLocal(booksProxy[i]);
}
return booksLocal;
}
public void PurchaseBook(string storeId,
string orderNumber,
int quantity, local.PaymentType paymentType,
local.Book book,
bool online,
bool onsite)
{
service.RetailOpsClient proxy;
//Map local Book to proxy Book
service.Book bookProxy = MapLocalBookToProxy(book);
// Map Payment Terms
service.PaymentType paymentTypeProxy =
LibroLogic.UI.ServiceGateways.localhost.
PaymentType.Hold;
if (online & onsite)
{
proxy = new LibroLogic.UI.
ServiceGateways.localhost.
RetailOpsClient("RetailTCP");
}
else if (online & !onsite)
{
proxy = new LibroLogic.UI.
ServiceGateways.localhost.
RetailOpsClient("RetailHTTP");
}
else
{
proxy = new LibroLogic.UI.ServiceGateways.
localhost.RetailOpsClient("RetailMSMQ");
}
proxy.PurchaseBook(storeId, orderNumber, quantity,
paymentTypeProxy, bookProxy);
}
private static service.Book
MapLocalBookToProxy(local.Book book)
{
service.Book bookProxy = new
LibroLogic.UI.ServiceGateways.localhost.Book();
bookProxy.Advance = book.Advance;
bookProxy.Notes = book.Notes;
bookProxy.OnHand = book.OnHand;
bookProxy.Price = book.Price;
bookProxy.Pubdate = book.Pubdate;
bookProxy.PubId = book.PubId;
bookProxy.Royalty = book.Royalty;
bookProxy.Title = book.Title;
bookProxy.TitleId = book.TitleId;
bookProxy.Type = book.Type;
bookProxy.YtdSales = book.YtdSales;
return bookProxy;
}
private static local.Book
MapProxyBookToLocal(service.Book bookProxy)
{
local.Book book = new
LibroLogic.UI.BusinessEntities.Book();
book.Advance = bookProxy.Advance;
book.Notes = bookProxy.Notes;
book.OnHand = bookProxy.OnHand;
book.Price = bookProxy.Price;
book.Pubdate = bookProxy.Pubdate;
book.PubId = bookProxy.PubId;
book.Royalty = bookProxy.Royalty;
book.Title = bookProxy.Title;
book.TitleId = bookProxy.TitleId;
book.Type = bookProxy.Type;
book.YtdSales = bookProxy.YtdSales;
return book;
}
#endregion
}
}
Binding | Interoperable | Disconnected Support | Reliable |
---|---|---|---|
WsHttpBinding | Yes | No | Depends |
NetTcpBinding | No | No | Yes |
NetMsmqBinding | No | Yes | Yes |