Walk through coding a sample app consisting of reusable components that are WCF services
|125163 EmployeeSelfProcurement code.zip|
Service Oriented Architecture (SOA) captures some of the key principles in implementing large-scale, distributed systems. Many of these patterns and principles (e.g., composition and service reusability) involve services calling other services. In this article, we will use Windows Communication Foundation (WCF) and the .NET Framework to implement a system in which services are calling other services. The solution will focus on implementing best practices and showcasing a few key patterns. The solution will also show how to leverage some of the new technologies introduced in .NET 4 to implement and test our system. We'll start with a simple and straightforward implementation in this article—part 1, then in part 2 incrementally improve certain areas of the initial solution by implementing their functionality in a more effective manner.
The system we will use to demonstrate these best practices and key patterns is the back end of a self-procurement website. The website allows employees to browse a catalog of office equipment products and order them online. After an employee selects a product to order, the website calls our back-end system to process the order.
At a high level, the back-end infrastructure, depicted in green in Figure 1, comprises three main components. Each of the components is implemented as a separate, autonomous service. The Procurement Service is used by the website to process purchase requests. The Procurement Service uses two other services to implement its functionality: the Spending Limit Service and the Inventory Service.
The Procurement Service is responsible for orchestrating the processing of the purchase request. It implements the business process involved in fulfilling the request by tying together the different components that take part in the process.
The company implements a governing mechanism to protect against monetary abuses. It does this by placing a limit on the amount of money that an employee can spend over a given time period. The Spending Limit Service enforces this limit across all the business processes where an employee can spend money. The Spending Limit Service implements an interface that allows trusted callers (typically, other systems) to provide an employee's name to retrieve the remaining spending limit for that employee. It also implements the Reservation Pattern, allowing clients to place a temporary lock on a portion of the user's remaining budget and eventually to commit or release that lock.
The Inventory Service manages the inventory of office supplies. Employees can only order items that are currently in stock, and it is the Inventory Service's job to ensure that every order made on the site can be fulfilled. The Inventory Service implements the Reservation Pattern, allowing clients to take a temporary lock on certain items in stock and eventually commit or release the lock.
The overall interaction is as follows:
- The website calls the Procurement Service.
- The Procurement Service calls the Spending Limit Service to verify that the order is within the employee's spending limit.
- If that is OK, then the Procurement Service reserves the product with the Inventory Service and reserves the funds required to procure it with the Spending Limit Service.
- The Procurement Service verifies the responses from both services, and if both reservations are successful it confirms the reservations. Otherwise, it cancels the reservations.
- Finally, the Procurement Service replies back to the website.
Implementing the Scenario
The solution is implemented using the three main services as mentioned. Each service is implemented using two projects: a Visual Studio 2010 WCF Service Library project and a Windows console application project.
The WCF Service Library projects generate DLLs that hold the implementations of the services. This allows us the flexibility to host the service in multiple hosting environments, such as Microsoft IIS, a Windows service, or in our case, a Windows console application.
The Windows console application project is used to host the service in a console application. This will allow us maximum flexibility and control over the hosting environment while developing our application and also allow us to demonstrate a few things, as you'll see later on.
Together, our solution comprises of a total of seven projects. You can download the source files for the solution by clicking the Download the Code Here link at the top of this article.
The Test Client
Let's start by inspecting the client application. The client application mimics the functionality of the website by making service calls to the Procurement Service. It also measures the service's response time. Measuring will help us understand how the different techniques applied throughout the article will affect overall system performance and responsiveness.
When making calls to the Procurement Service, the website impersonates the employee using the site. It thus gives the Procurement Service access to the identity of the employee using the website (for more details on how to implement Impersonation with ASP.NET, see msdn.microsoft.com/en-us/library/xh507fc5(VS.100).aspx). User identities are transferred among the back-end systems as application date.
Let's look at the implementation of the test client, shown in Figure 2. In our first implementation, the Main method waits for the user to press the Enter key before starting the testing process. In a real-world environment, the services are running all the time. Clients in that real-world environment can just connect to these ever-present services when they start up. In our testing scenario, we'll be starting the services together with the client, and to prevent test failures due to connection timeouts as the services spin up, we would like to ensure that the services are up and available before we start our testing. This is a common practice in manual testing. Another common approach, used more in fully automated test environments, is to insert a long delay in the test startup sequence. What we have here is a synchronization problem, which I'll address in part 2 of this article.
Once the user presses the Enter key, the RunTest method is called. This method implements the actual test logic, which we'll take a closer look at shortly.
Finally, when the test is completed, the program checks to see whether it's running under a debugger. If it is, the program prompts the user to press the Enter key before shutting down. Without this check, the application would either wait for input when run from the command line (unnecessary) or close the console window when running in Visual Studio before we can see the results (not usable). This is a good technique to use if you expect to run a console application both under a debugger and from the command line. In addition, when running without debugging in Visual Studio 2010, the console window displays a message and waits for us to press any key before closing the console window, so there's no need for us to add an unnecessary request for input before shutting down.
Tip: Consider using Debugger.IsAttached to determine how to end the run of a console application.
Some of you may be wondering at this point why I implemented the test this way, instead of using a test harness and proper unit tests. The reason is that I wanted to leave maximum flexibility for demonstrating different WCF techniques in this example, and allow you to more easily play with the code afterward. In real-world testing scenarios, where testing is best done using a test-automation framework, I highly recommend using a test-automation infrastructure such as the one built into Visual Studio 2010 and Team Foundation Server 2010.
Next, let's see what the RunTest method, shown in Figure 3, does. As you remember, the web application uses impersonation to send the credentials of the employee using the website to the self-procurement service. In the test code, we are applying the client credentials in code, so that the call is made under a different account than the one being used to run the applications.
This is a general good practice to adopt, as most developers run as Administrators on their machines, and certain assumptions may break when not running under the developer's Administrator credentials. In addition, in a real-world scenario the client and service practically never run under the same user account. I've created a few non-administrator accounts on my development machine which I use to test services whose functionality (or the functionality of resources that they are using directly or indirectly) makes use of the caller's identity.
Tip: Consider creating non-administrator accounts on your developer machine, or creating well-known test accounts in your test environment which your development team can share, and using these accounts when testing services whose functionality makes use of the caller's identity.
The service is exposed over a NetTcpBinding, configured with the default settings. By default the NetTcpBinding uses the Transport security mode, client credential type of Windows, and the EncryptAndSign protection level. This means that unless you change things, every time you use a TCP transport you're passing the caller's credentials to the service. In addition to being a potential privacy issue (if you're not aware of it), passing the credentials also increases the communication overhead in cases where it is not required (e.g., when communicating with a service that does not care about credentials inside a protected private network).
Tip: Security is very important! However, when security is absolutely not required, consider turning some or all of the security settings off to increase the performance of TCP and other transports. When creating TCP bindings in code, you can do it like this: NetTcpBinding(SecurityMode.None).
The rest of the code is pretty straightforward, with only two notable things. First, please note that we are using the System.Diagnostic.Stopwatch class to measure the time it takes for the test to run. If the installed hardware and operating system support a high-resolution performance counter, then the Stopwatch class uses that counter to measure elapsed time. Otherwise, the Stopwatch class uses the system timer to measure elapsed time.
Tip: When timing operations, consider using the System.Diagnostic.Stopwatch class to measure the length of time that an operation used, as it can potentially provide greater accuracy.
Second, please note that we are not counting the results of the first iteration of the loop in the average calculations. This is a common practice when timing distributed systems since startup time can be significant. In our solution we need to wait for connections to be created and services to spin up, which takes a long time. Once this initial performance hit is done, things run along much more smoothly.
Tip: To eliminate various random factors, run your tests multiple times, ignore the first run (especially in cold-start scenarios), and average your results. This advice obviously does not apply when measuring startup times.
This is not only a test issue, as clients may experience this issue in the deployment environment following a system restart. I'll address this in the part 2 article.
The Service Hosts
The host applications for the three services all follow the same format. Let's examine the code for the host application for the Procurement Service, shown in Figure 4. The code for the hosting application is very simple. All it does is create a new service host for the service, print out the endpoints at which the service is listening, start the service, and wait for the user to press the Enter key in order to shut down the service.
The service is configured using a configuration file. The same configuration file (not identical copies, but the actual file) is shared between the service host project and the service project. In Visual Studio this is accomplished by adding the App.config file to the hosting project as a link. Using the same configuration file for both the service implementation and the test hosting code creates a "single source of truth" for the configuration. This can help ensure that the correct service settings (the ones that the service was tested with) are used when deploying the service implementation to the deployment environment.
Writing the endpoint addresses to the screen is very helpful and a recommended best practice. First, it lets you see if something was configured incorrectly when the service starts up. Second, it tells you where the service is hosted, so that you can easily point a client at it. This is very helpful if you know the binding and would like to use a channel factory to talk to the service. Finally, if your service exposes metadata, this would print out the metadata address. This address can be used by the SvcUtil.exe command-line utility and Visual Studio to generate client code. It can also be used by the WCF test client to get the metadata and create an ad hoc client.
Tip: Consider writing out the endpoints that your service is listening on.
In this case, the host is a Windows console application, so it was easy to print out the endpoint addresses. When hosting a service as a Windows service, we may not have an interactive session to print the data to. In that case, consider writing the endpoints out to a file stored at a well-known location.
As you can see in the console application's screenshot in Figure 5, the naming convention that we used makes it fairly easy to figure out the protocol and contract from the endpoint address. If this is not the case in your situation, simply print out all the information that a developer (including you) might need in order to connect to the service.
The Self-Procurement Service
The self-procurement service implements a simple interface for making a purchase request. It has one method that takes a PurchaseRequest object and returns true if the purchase object was processed successfully and false otherwise.
public interface IProcurement
bool MakePurchaseRequest(PurchaseRequest request);
Since this is only a sample application, we will keep the interface as simple as possible. A real-world implementation would obviously provide methods for querying the status of a purchase request, listing the current or past purchase requests, and deleting a purchase request. However, the implementation of MakePurchaseRequest that we'll explore covers all the details used for the real-world implementation of the other methods.
public class PurchaseRequest
public double CostPerUnit;
public int NumberOfUnits;
public string ItemId;
In addition, to keep things simple, our system only accepts orders for one or more units of the same product (for example, one office chair or five pens). However, it is easy to see how the systems could be extended to support the ordering of multiple different products in the same order.
Let's look at the implementation of ProcurementService, which Figure 6 shows. The class contains three methods: a constructor, a destructor, and the implementation of the service operation. The constructor and destructor manage the connections to the other services. Closing the client explicitly is very important, since the other services would not close gracefully until all their incoming connections are closed. If the connection is not closed, when the service hosts of the other services shut down they will wait for their incoming connections to close, and since they were not closed gracefully, they'll eventually time out and fault.
Tip: To ensure graceful service shutdown, remember to always close your outgoing client connections.
The service implementation is more complex than the constructor and destructor, as it implements the business logic of the service. It first does some basic argument verification. (In a real-world implementation, it's recommended to perform deeper verification that will inspect the values of purchase requests for illegal values, such as a negative number of products or negative cost.) It then checks the spend limit against the total purchase amount. Finally, it reserves the items and funds, and if both reservations were successful, it commits the reservations.
Looking at How the System Is Connected
The Procurement Service uses two outbound connections, one to the Inventory Service and one to the Spending Limit Service. Since our service implementation has no constructor, the two client proxies are created using member initializers. You may ask yourself, isn't this wasteful? Why not wait until the service is called for the first time to create the client proxies? The reality is that with a plain TCP connection that has no other binding elements enabled, no network resources are consumed until the first message is sent. Even calling Open on the channel doesn't create an actual socket connection. To demonstrate this, we will use a handy system utility called Windows Resource Monitor (resmon.exe).
As you can see in Figure 7, the services are up and listening, but there are no active TCP connections. Once we start the test client, several connections are created, as Figure 8 shows. Since the client and all the services are on the same machine, we have a complete view of the system, and if we look closely and match the local and remote ports we can see how the system is woven together. The two lines that indicate "System" in the Image column are for the HTTP connections that are managed by HTTP.SYS (an element of the operating responsible for HTTP traffic) on behalf of the two services in our system that are exposed over HTTP.
If we wait a little while before allowing the client to close its connection (for example, if we put a breakpoint on client.Close() in the RunTest method), we can also see that the HTTP connections are torn down after a period of inactivity.
Tip: Resource Monitor can provide great insight into how the system is connected and how the connections behave over time. When constructing a complex system, consider using Resource Monitor to inspect your system's network utilization and ensure that connections are made correctly and torn down when you expect them to.
The Spending Limit Service
The Spending Limit Service, which Figure 9 shows, implements a simple interface for managing an employee's spending limit. It has one method that can be used to obtain the employee's remaining spending limit and three methods to implement the Reservation Pattern. The ReserveFunds method is used to reserve funds for a user, and makes use of the RemainingBalanceTooLowFault class to report an error if the requested amount is larger than the available limit for that user.
In our sample application, to reduce complexity and make things simpler to set up, the spending limit will be a hard-coded number, and the balance will be kept in memory. In a real-world application, there would probably be a monthly and/or a yearly limit on the amount of money that an employee can spend, and managing this limit would most likely involve interacting with a database. When a database is involved, it is also likely that most of the reservation-management operations would be implemented using database constructs (e.g., stored procedures). We would simulate this database interaction in our implementation by introducing artificial delays. For the sake of simplicity, we'll assume that each database operation takes 100ms.
The in-memory implementation uses two collections to manage employees' spending limits. The first collection, SpendingLimits, holds all the current limits, and the second collection, FundsReserved, holds fund reservations until they are committed (or until they expire).
The Spending Limit Service's implementation of the Reservation Pattern holds reservations for a predetermined amount of time, as shown in Figure 10. If the reservation is committed within that time it will be honored; if it is committed within that time period and double that time period, it may or may not be honored, and after two time periods it will not be honored. In this sample implementation, that time period is hard-coded at 5 minutes, which should allow us to debug the system, set breakpoints, and walk through the code, without having reservations time-out from under us. In a real-world implementation of this kind of system, reservations would probably be held for somewhat shorter periods of time. The timeout mechanism is implemented using an instance of the Timer class, which periodically calls the SweepOldReservations method to clean out expired reservations.
As you can see in Figure 10, the implementation uses aggressive locking since the default instancing mode for the service is Per-Call. This means that multiple instances of the service can access the shared data structures simultaneously, and we need to protect against that. In a real-world implementation (using a database), concurrency management would manifest itself as transactions. However, there are ways to improve on this, and we will explore them in the upcoming part 2 article.
You may have also noticed the use of the new .NET 4 class: Tuple. Using this class saved us from defining a new type that would only be used in four places in the code. This, however, comes on the expense of clarity, since we now need to use the generic Item1 and Item2 to access the Tuple members instead of more the descriptive names we would use if we implemented our own data structure. Because we're only using these members in three places in the code, since these places are in the same file, and since the types of the Tuple members are distinct, it makes sense to use it here.
The Inventory Service
The inventory management service, shown in Figure 11, implements a simple interface which uses three methods to implement the Reservation Pattern for inventory items. This design is similar to that of the spending-limit service API.
public interface IInventoryReservation
Guid ReserveItems(string itemId, int numberOfItems);
void CommitReservation(Guid reservationId);
\\[OperationContract(IsOneWay = true)\\]
void CancelReservation(Guid reservationId);
Similarly to the Spending Limit Service, to reduce complexity and make things simpler to set up, the inventory management service's implementation will use a randomly generated number for the current stock level of the requested item. In a real-world application, this information would most likely be stored in a database, and the reservation-management operations would be implemented using database constructs (e.g., stored procedures). We would simulate this database interaction in our implementation by introducing artificial delays. For the sake of simplicity, we'll once again assume that each database operation takes 100ms.
The in-memory implementation uses a single collection to manage reservations, to hold reservations until they are committed (or until they expire). The inventory service's implementation of the Reservation Pattern is very similar to that of the spending-limit service in terms of how long it holds reservations and how they expire. The expiration period is again hard coded at 5 minutes, for the same reasons previously mentioned. Since we do not manage actual inventory levels, and to keep things simple once again, we will not implement the actual inventory tracking.
More to Come
This concludes the first part of the article. In this part we reviewed the problem domain, the solution's architecture, and each of the elements in the implementation of this sample application. We built a plausible implementation, gathered helpful tips along the way, and now we're at the point where we're ready to start testing our solution.
In the second part of this article, we'll test the solution and examine its performance characteristics. We will then start improving on this solution by using new .NET 4 constructs and additional architectural patterns. Specifically, we will:
- Provide an effective solution for the synchronization problem when starting the test.
- Improve our test coverage by introducing a simple and efficient technique for launching multiple clients that will access the service simultaneously (thus testing our concurrency-management code, which we did not address in this implementation of the test client).
- Reduce network utilization and increase overall service performance by using a different paradigm for implementing cross-service consistency.
- Parallelize independent operations in a call-efficient and thread-efficient way.
- Optimize service response time and resource utilization.
- Speed up the system's startup time.
Shy Cohen (www.ShyCohen.com) is an independent software consultant focusing on cloud computing, distributed systems, and software architecture. Prior to starting his independent practice, Shy worked as a senior program manager at Microsoft and helped design and ship Windows Communication Foundation (WCF).