Managing State in a WCF Service


It makes sense to look at how to manage and maintain state in a WCF service first and then return to the issue of sequencing operations later.

The exercises that you performed in previous chapters involved stateless operations. All the information required to perform an operation in the ProductsService service has been passed in as a series of parameters by the client application. When an operation has completed, the service “forgets” that the client ever invoked it! In the shopping cart scenario, the situation is different. You must maintain the shopping cart between operations. In the exercises in this section, you will learn that this approach requires a little thought and careful design.

Create the ShoppingCartService service

  1. Using Visual Studio 2005, create a new project. Select the .NET Framework 3.0 project types under Visual C# and use the WCF Service Library template. Name the project ShoppingCartService and save it in the Microsoft Press\WCF Step By Step\Chapter 7 folder under your \My Projects folder.

  2. In Solution Explorer, rename the Class1.cs file as image from book ShoppingCartService.cs.

  3. In the code view window displaying the image from book ShoppingCartService.cs file, delete all the comments and code apart from the using statements at the top of the file.

  4. In Solution Explorer, add references to the Microsoft.Practices.EnterpriseLibrary.Data.dll, Microsoft.Practices.EnterpriseLibrary.Common.dll, and Microsoft.Practices.ObjectBuilder.dll assemblies located in the C:\Program Files\Microsoft Enterprise Library\bin folder.

  5. In the code view window displaying image from book ShoppingCartService.cs, add the following using statements to the list at the top of the file:

     using Microsoft.Practices.EnterpriseLibrary.Data; using System.Data;

  6. Add a namespace definition for the ShoppingCartService namespace to the file, underneath the using statements. The file should look like this:

     using System; using System.Collections.Generic; using System.Text; using System.ServiceModel; using System.Runtime.Serialization; using Microsoft.Practices.EnterpriseLibrary.Data; using System.Data; namespace ShoppingCartService { } 

  7. Add the following data structure shown in bold to the ShoppingCartService namespace:

     namespace ShoppingCartService {     // Shopping cart item     class ShoppingCartItem     {         public string ProductNumber;         public string ProductName;         public decimal Cost;         public int Volume;     } }

    This class defines the items that can be stored in the shopping cart. A shopping cart will contain a list of these items. Notice that this is not a data contract; this class is for internal use by the service. If a client application queries the contents of the shopping cart, the service will send it a simplified representation as a string. In this way, there should be no dependencies between the structure of the shopping cart and the client applications that manipulate them.

  8. Add the following service contract to the ShoppingCartService namespace, after the ShoppingCartItem class:

     namespace ShoppingCartService {     …     [ServiceContract(Namespace = "http://adventure-works.com/2007/03/01",                      Name = "ShoppingCartService")]     public interface IShoppingCartService     {         [OperationContract(Name="AddItemToCart")]         bool AddItemToCart(string productNumber);         [OperationContract(Name = "RemoveItemFromCart")]         bool RemoveItemFromCart(string productNumber);         [OperationContract(Name = "GetShoppingCart")]         string GetShoppingCart();         [OperationContract(Name = "Checkout")]         bool Checkout();     } }

    The client application will invoke the AddItemToCart and RemoveItemFromCart operations to manipulate the shopping cart. In the AdventureWorks database, items are identified by their product number. To add more than one instance of an item requires invoking the AddItemToCart operation once for each instance. These operations return true if they are successful, false otherwise.

    The GetShoppingCart operation will return a string representation of the shopping cart contents that the client application can display.

    The client application will call the Checkout operation if the user wants to purchase the goods in the shopping cart. Again, this operation returns true if it is successful, false otherwise.

    Note 

    For the purposes of this example, assume that the user has an account with Adventure-Works, and so the Checkout operation simply arranges dispatch of the goods to the customer’s address. The customer will be billed separately.

  9. Add the following class to the ShoppingCartService namespace:

     namespace ShoppingCartService {     …     public class ShoppingCartServiceImpl : IShoppingCartService     {     } }

    This class will implement the operations for the IShoppingCartService interface.

  10. Add the shoppingCart variable shown below to the ShoppingCartServiceImpl class:

     public class ShoppingCartServiceImpl : IShoppingCartService {     private List<ShoppingCartItem> shoppingCart =         new List<ShoppingCartItem>(); }

    This variable will hold the user’s shopping cart, comprising a list of ShoppingCartItem objects.

  11. Add the private method shown below to the ShoppingCartServiceImpl class:

     // Examine the shopping cart to determine whether an item with a // specified product number has already been added. // If so, return a reference to the item, otherwise return null private ShoppingCartItem find(List<ShoppingCartItem> shoppingCart,                               string productNumber) {         foreach (ShoppingCartItem item in shoppingCart)         {             if (string.Compare(item.ProductNumber, productNumber) == 0)             {                 return item;             }         }         return null; }

    This AddItemToCart and RemoveItemFromCart operations will make use of this utility method.

    Note 

    The code for this method is available in the file image from book Find.txt located in the Chapter 7 folder.

  12. Implement the AddToCart method in the ShoppingCartServiceImpl class, as shown below:

     public bool AddItemToCart(string productNumber) {     // Note: For clarity, this method performs very limited security     // checking and exception handling     try     {         // Check to see whether the user has already added this         // product to the shopping cart         ShoppingCartItem item = find(shoppingCart, productNumber);          // If so, increment the volume         if (item != null)         {             item.Volume++;             return true;         }         // Otherwise retrieve the details of the product from the database         else         {             // Connect to the AdventureWorks database             Database dbAdventureWorks =                 DatabaseFactory.CreateDatabase("AdventureWorksConnection");             // Retrieve the details of the selected product             string queryString = @"SELECT Name, ListPrice                             FROM Production.Product                             WHERE ProductNumber = '" + productNumber + "'";             IDataReader productsReader =              dbAdventureWorks.ExecuteReader(CommandType.Text, queryString);             // Check to see whether the user has already added             // this product to the shopping cart             // Create and populate a new shopping cart item             ShoppingCartItem newItem = new ShoppingCartItem();             if (productsReader.Read())             {                 newItem.ProductNumber = productNumber;                 newItem.ProductName = productsReader.GetString(0);                 newItem.Cost = productsReader.GetDecimal(1);                 newItem.Volume = 1;                 // Add the new item to the shopping cart                 shoppingCart.Add(newItem);                 // Indicate success                 return true;             }             else             {                 // No such product in the database                 return false;             }         }     }     catch (Exception)     {         // Indicate failure         return false;     } }

    Note 

    The code for this method is available in the file image from book AddItemToCart.txt located in the Chapter 7 folder.

    For clarity, this method does not perform any security checking (such as protecting against SQL injection attacks) and exception handling is minimal. In a production application, you should address these aspects robustly, as described in the preceding chapters.

  13. Add the RemoveItemFromCart method shown below to the ShoppingCartServiceImpl class:

     public bool RemoveItemFromCart(string productNumber) {     // Determine whether the specified product has an     // item in the shopping cart     ShoppingCartItem item = find(shoppingCart, productNumber);     // If so, then decrement the volume     if (item != null)     {         item.Volume--;         // If the volume is zero, remove the item from the shopping cart         if (item.Volume == 0)         {             shoppingCart.Remove(item);         }         // Indicate success         return true;     }     // No such item in the shopping cart     return false; }

    Note 

    The code for this method is available in the file image from book RemoveItemFromCart.txt located in the Chapter 7 folder.

  14. Implement the GetShoppingCart method in the ShoppingCartServiceImpl class, as follows:

     public string GetShoppingCart() {     // Create a string holding a formatted representation     // of the shopping cart     string formattedContent = "";     decimal totalCost = 0;     foreach (ShoppingCartItem item in shoppingCart)     {         string itemString = String.Format(                "Number: {0}\tName: {1}\tCost: {2}\tVolume: {3}",                item.ProductNumber, item.ProductName, item.Cost,                item.Volume);         totalCost += (item.Cost * item.Volume);         formattedContent += itemString + "\n";     }     formattedContent += "\nTotalCost: " + totalCost;     return formattedContent; }

    Note 

    The code for this method is available in the file GetShoppingCart.txt located in the Chapter 7 folder.

    This method generates a string describing the contents of the shopping cart. The string contains a line for each item, with the total cost of the items in the shopping cart at the end.

    Note 

    Although not shown in this method, you should format numeric values containing monetary items using the current locale.

  15. Add the Checkout method to the ShoppingCartServiceImpl class, as follows:

     public bool Checkout() {     // Not currently implemented - just return true     return true; }

    This method is simply a placeholder. In a production system, this method would perform tasks such as arranging the dispatch of items, billing the user, and updating the database to reflect the changes in stock volume according to the user’s order.

  16. Build the solution.

You now need to build a host application for this service. You will use a simple console application for this purpose.

Create a host application for the ShoppingCartService service

  1. Add a new project to the ShoppingCartService solution. Select the Windows project types under Visual C# and use the Console Application template. Name the project ShoppingCartHost and save it in the Microsoft Press\WCF Step By Step\Chapter 7\ShoppingCartService folder under your \My Projects folder.

    Tip 

    In the File menu, point to Add, and then click New Project to add a new project to a solution.

  2. Add the image from book App.config file located in the Microsoft Press\WCF Step By Step\Chapter 7\Config folder under your \My Projects folder to the ShoppingCartHost project.

    This configuration file currently just contains the definition of the connection string that the service uses for connecting to the AdventureWorks database.

    Tip 

    In the Project menu, click Add Existing item to add a file to a project. You will also need to select All Files (*.*) in the Files of type dropdown in the Add Existing Item dialog box to display the image from book App.config file.

  3. Edit the image from book App.config file by using the WCF Service Configuration Editor. In the right pane, click Create a New Service.

    The New Service Element Wizard starts.

  4. In the “What is the service type of your service?” page, click Browse. In the Type Browser window, move to the ShoppingCartService\ShoppingCartService\bin\Debug folder under the Chapter 7 folder, and double-click the ShoppingCartService.dll assembly. Select the ShoppingCartService.ShoppingCartServiceImpl type, and then click Open. In the “What is the service type of your service?” page, click Next.

  5. In the “What service contract are you using?” page, select the ShoppingCartService.IShoppingCartService contract, and then click Next.

  6. In the “What communications mode is your service using?” page, click HTTP, and then click Next.

  7. In the “What method of interoperability do you want to use?” page, select Advanced Web Services interoperability, select Simplex communication, and then click Next.

  8. In the “What is the address of your endpoint?” page, enter http://localhost:9000/ShoppingCartService/ShoppingCartService.svc and then click Next.

  9. In the “The wizard is ready to create a service configuration” page, click Finish.

    The wizard adds the service to the configuration file, and creates an endpoint definition for the service.

  10. Save the configuration file, and then exit the WCF Service Configuration Editor. Allow Visual Studio 2005 to reload the updated image from book App.config file. The <system.serviceModel> section should look like this:

     <system.serviceModel>   <services>     <service name="ShoppingCartService.ShoppingCartServiceImpl">       <endpoint address= "http://localhost:9000/ShoppingCartService/ShopppingCartService.svc"         binding="wsHttpBinding" bindingConfiguration="" contract="ShoppingCartService. IShoppingCartService" />     </service>   </services> </system.serviceModel>

  11. In Solution Explorer, add a reference to the System.ServiceModel assembly and a reference to the ShoppingCartService project to the ShoppingCartHost project.

  12. Edit the image from book Program.cs file in the ShoppingCartHost project. Add the following using statement to the list at the top of the file:

     using System.ServiceModel;

  13. Add the statements shown below in bold to the Main method in the Program class:

     class Program {     static void Main(string[] args)     {         ServiceHost host = new ServiceHost(             typeof(ShoppingCartService.ShoppingCartServiceImpl));         host.Open();         Console.WriteLine("Service running");         Console.WriteLine("Press ENTER to stop the service");         Console.ReadLine();         host.Close();     } }

    This code creates a new instance of the ShoppingCartService service, listening on the HTTP endpoint you specified in the configuration file.

The next task is to build a client application to test the ShoppingCartService service. You will create another Console application to do this.

Create a client application to test the ShoppingCartService service

  1. Add another new project to the ShoppingCartService solution. Select the Windows project types under Visual C#, and select the Console Application template. Name the project ShoppingCartClient and save it in the Microsoft Press\WCF Step By Step\Chapter 7\ShoppingCartService folder under your \My Projects folder.

  2. In the ShoppingCartClient project, add a reference to the System.ServiceModel assembly.

  3. Generate a proxy class for the client application by using the following procedure:

    • Open a Visual Studio 2005 Command Prompt window and move to the ShoppingCartService\ShoppingCartService\bin\Debug folder in the Microsoft Press\WCF Step By Step\Chapter 7 folder under your \My Projects folder

    • In the Visual Studio 2005 Command Prompt window, run the command:

       svcutil ShoppingCartService.dll

    • Run the command:

       svcutil /namespace:*,ShoppingCartClient.ShoppingCartService adventure- works.com.2007.03.01.wsdl *.xsd /out:ShoppingCartServiceProxy.cs

  1. Return to Visual Studio 2005, and add the image from book ShoppingCartServiceProxy.cs file in the ShoppingCartService\ShoppingCartService\bin\Debug folder to the ShoppingCartClient project.

    Note 

    You can also use the Add Service Reference command in the Project menu to generate the proxy for an HTTP service and add it to the client project. However, the service must be running for Visual Studio 2005 to be able to do this, and the service must enable metadata publishing (by default, this feature is disabled). See Chapter 1, “Introducing Windows Communication Foundation,” for details on how to do this.

  2. Add a new application configuration file to the ShoppingCartClient project. Name this file image from book App.config.

    Tip 

    To add a new file to a project, on the Project menu, click Add New Item.

  3. Edit the image from book App.config file in the ShoppingCartClient project by using the WCF Service Configuration Editor. In the left pane, click the Client folder and then click Create a New Client in the right pane.

    The New Client Element Wizard starts.

  4. In the “What method do you want to use to create the client?” page, select From service config. Click Browse by the Config file text box. In the Open dialog box, move to the Chapter 7\ShoppingCartService\ShoppingCartHost folder, select the image from book app.config file, and then click Open. In the “What method do you want to use to create the client?” page click Next.

  1. In the “Which service endpoint do you want to connect to?” page, accept the default service endpoint and then click Next.

  2. In the “What name do you want to use or the client configuration?” page, type HttpBinding_ShoppingCartService and then click Next.

  3. In the “The wizard is ready to create a client configuration” page, click Finish.

    The wizard adds the client definition to the configuration file and creates an endpoint called HttpBinding_ShoppingCartService that the client application can use to connect to the ShoppingCartService service. However, the name of the type implementing the contract in the client proxy has a different name from that used by the service, so you must change the value added to the client configuration file.

  4. Click the HttpBinding_ShoppingCartService endpoint in the left pane. In the right pane, set the Contract property to ShoppingCartClient.ShoppingCartService.ShoppingCartService (the type is ShoppingCartService in the ShoppingCartClient.ShoppingCartService namespace in the client proxy).

  5. Save the configuration file, and exit the WCF Service Configuration Editor. Allow Visual Studio 2005 to reload the modified image from book App.config file. The image from book App.config file for the ShoppingCartClient application should look like this:

     <?xml version="1.0" encoding="utf-8" ?> <configuration>   <system.serviceModel>     <client>       <endpoint address="http://localhost:9000/ShoppingCartService/ ShoppingCartService.svc"           binding="wsHttpBinding" bindingConfiguration="" contract="ShoppingCartClient .ShoppingCartService.ShoppingCartService"           name="HttpBinding_ShoppingCartService">           <identity>             <certificateReference storeName="My" storeLocation="LocalMachine" x509Find Type="FindSubjectDistinguishedName" />           </identity>       </endpoint>     </client>   </system.serviceModel> </configuration>

    Remove the <identity> element, and its child <certificateReference> element from the configuration file. This version of the service does not use certificates.

  6. In Visual Studio 2005, edit the image from book Program.cs file in the ShoppingCartClient project. Add the following using statements to the list at the top of the file.

     using System.ServiceModel; using ShoppingCartClient.ShoppingCartService;

  7. Add the statements below, shown in bold, to the Main method of the Program class:

     static void Main(string[] args) {     Console.WriteLine("Press ENTER when the service has started");     Console.ReadLine();     try     {         // Connect to the ShoppingCartService service         ShoppingCartServiceClient proxy =         new ShoppingCartServiceClient("HttpBinding_ShoppingCartService");         // Add two water bottles to the shopping cart         proxy.AddItemToCart("WB-H098");         proxy.AddItemToCart("WB-H098");         // Add a mountain seat assembly to the shopping cart         proxy.AddItemToCart("SA-M198");         // Query the shopping cart and display the result         string cartContents = proxy.GetShoppingCart();         Console.WriteLine(cartContents);         // Disconnect from the ShoppingCartService service         proxy.Close();     }     catch (Exception e)     {         Console.WriteLine("Exception: {0}", e.Message);   }     Console.WriteLine("Press ENTER to finish");     Console.ReadLine(); }

    Note 

    Complete code for the Main method is available in the file image from book Main.txt located in the Chapter 7 folder.

    The code in the try block creates a proxy object for communicating with the service. The application then adds three items to the shopping cart–two water bottles and a mountain seat assembly–before querying the current contents of the shopping cart and displaying the result.

  8. In Solution Explorer, right-click the ShoppingCartService solution and then click Set StartUp Projects. In the right pane of the Solution ‘ShoppingCartService’ Property Pages dialog box, select Multiple startup projects, and set the Action property for the ShoppingCartClient and ShoppingCartHost projects to Start. Click OK.

  9. Start the solution without debugging. In the client console window displaying the message “Press ENTER when the service has started,” press Enter.

    Note 

    If a Windows Security Alert message box appears, click Unblock to allow the service to use HTTP port 9000.

    The client application adds the three items to the shopping cart and displays the result, as shown in the following image:

    image from book

  1. Press Enter to close the client application console window. In the host application console window, press Enter to stop the service.

This technique for maintaining the shopping cart in the service appears to work well. But, this is one of those situations that should leave you feeling a little bit suspicious, as everything appears to be just a bit too easy!

Service Instance Context Modes

If you think for a minute about what is going on, the service creates an instance of the shopping cart when an instance of the service is itself created by the host; the shoppingCart variable is a private instance variable in the ShoppingCartServiceImpl class. What happens if two clients attempt to use the service simultaneously? The answer is that each client gets their own instance of the service, with its own instance of the shoppingCart variable. This is an important point. By default, the first time each client invokes an operation in a service, the host creates a new instance of the service just for that client. How long does the instance last? You can see from the shopping cart example that the instance hangs around between operation calls, otherwise it would not be able to maintain its state in an instance variable. The service instance is only destroyed after the client has closed the connection to the host (in true .NET Framework fashion, you do not know exactly how long the instance will hang around after the client application closes the connection because it depends on when the .NET Framework garbage collector decides it is time to reclaim memory). Now think what happens if you have 10 concurrent clients–you get 10 instances of the service. What if you have 10,000 concurrent clients? You get 10,000 instances of the service. If the client is an interactive application that runs for an indeterminate period while the user browses the product catalog and decides which items to buy, you had better be running the host application on a machine with plenty of memory!

Note 

If you are using the TCP, or named pipe transport, you can restrict the maximum number of concurrent sessions for a service by setting the MaxConnections property of the binding configuration. For these transports, the default limit is 10 connections. If you are using IIS to host a WCF service using the HTTP or HTTPS transports, you can configure IIS with the maximum number of concurrent connections it should allow–the details of how to do this are beyond the scope of this book, as it varies depending on which version of IIS you are using.

You can control the relationship between client applications and instances of a service by using the InstanceContextMode property of the ServiceBehavior attribute of the service. You specify this attribute when defining the class that implements the service contract, like this:

 [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)] public class ShoppingCartService : IShoppingCartService {     … }

The InstanceContextMode property can take one of the following three values: InstanceMode.PerSession, InstanceMode.PerCall, and InstanceMode.Single. The following sections describe these instance context modes.

The PerSession Instance Context Mode

The PerSession instance context mode specifies that the service instance is created when a client application first invokes an operation, and the instance remains active, responding to client requests, until the client application closes the connection, typically by calling the Close method on the proxy object. The time between invoking the first operation and closing the connection is referred to as the client application’s “session,” and the service instance is private to the session. Each time a client application creates a new session, it gets a new instance of the service. Two sessions cannot share a service instance when using this instance context mode, even if both sessions are created by the same instance of the client application.

It is possible for a client application to create multiple threads and then attempt to invoke operations in the same session simultaneously. By default, a service is single-threaded and will not process more than one request at a time. If a new request arrives while the service is still processing an earlier request, the WCF runtime causes the new request to wait for the earlier one to complete. The new request could possibly time-out while it is waiting to be handled. You can modify this behavior. The ServiceBehavior attribute has another property called ConcurrencyMode. You can set this property to specify how to process concurrent requests in the same session, like this:

 [ServiceBehavior(…, ConcurrencyMode = ConcurrencyMode.Single)] public class ShoppingCartService : IShoppingCartService {      }

The default value for this property is ConcurrencyMode.Single, which causes the service to behave as just described. You can also set this property to ConcurrencyMode.Multiple, in which case the service instance is multithreaded and can accept simultaneous requests. However, setting the Concurrency property to this value does not make any guarantees about synchronization. You must take responsibility for ensuring that the code you write in the service is thread-safe.

Note 

There is a third mode called ConcurrencyMode.Reentrant. In this mode, the service instance is single-threaded, but allows the code in your service to call out to other services and applications, which can then subsequently call back into your service. However, this mode makes no guarantees about the state of data in your instance of the service. It is the responsibility of your code to ensure that the state of service instance remains consistent, and that the service doesn’t accidentally deadlock itself.

The PerCall Instance Context Mode

The InstanceContextMode.PerCall instance context mode creates a new instance of the service every time the client application invokes an operation. The instance is destroyed when the operation completes. The advantage of this instance context mode is that it releases resources in the host between operations, greatly improving scalability. If you consider the situation with 10,000 concurrent users and the PerSession instance context mode, the main issue is that the host has to hold 10,000 instances of the service, even if 9,999 of them are not currently performing any operations because the users have gone to lunch without closing their copy of the client application and terminating their sessions. If you use the PerCall instance context mode instead, then the host will only need to hold an instance for the one active user.

The disadvantage of using this instance context mode is that maintaining state between operations is more challenging. You cannot retain information in instance variables in the service, so you must save any required state information in persistent storage, such as a disk file or database. It also complicates the design of operations, as a client application must identify itself so that the service can retrieve the appropriate state from storage (you will investigate one way of achieving this in an exercise later in this chapter).

You can see that the lifetime of a service instance depends on how long it takes the service to perform the requested operation, so keep your operations concise. You should be very careful if an operation creates additional threads, as the service instance will live on until all of these threads complete, even if the main thread has long-since returned any results to the client application; this can seriously affect scalability. You should also avoid registering callbacks in a service. Registering a callback does not block service completion, and the object calling back might find that the service instance has been reclaimed and recycled. The CLR traps this eventuality so it is not a security risk, but it is inconvenient to the object calling back as it will receive an exception.

The Single Instance Context Mode

The InstanceContextMode.Single instance context mode creates a new instance of the service the first time a client application invokes an operation and then uses this same instance to handle all subsequent requests from this client and every other client that connects to the same service. The instance is destroyed only when the host application shuts the service down. The main advantage of this instance context mode, apart from the reduced resource requirements, is that all users can easily share data. Arguably, this is also the principal disadvantage of this instance context mode!

The InstanceContextMode.Single instance context mode minimizes the resources used by the service at the cost of the same instance being expected to handle every single request. If you have 10,000 concurrent users, that could be a lot of requests. Also, if the service is single threaded (the ConcurrencyMode property of the ServiceBehavior attribute is set to ConcurrencyMode.Single), then expect many timeouts unless operations complete very quickly. Consequently, you should set the concurrency mode to ConcurrencyMode.Multiple and implement synchronization to ensure that all operations are thread-safe.

More Info 

Detailed discussion of synchronization techniques in the .NET Framework is outside the scope of this book, but for more information see the topic “Synchronizing Data For Multithreading” in the Microsoft Windows SDK Documentation or on the Microsoft Web site at http://msdn2.microsoft.com/en-us/library/z8chs7ft.aspx.

In the next exercise, you will examine the effects of using the PerCall and Single instance context modes.

Investigate the InstanceContextMode property of the ServiceBehavior

  1. In Visual Studio 2005, edit the image from book ShoppingCartService.cs file in the ShoppingCartService project.

  2. Add the ServiceBehavior attribute to the ShoppingCartServiceImpl class, with the InstanceContextMode property set to InstanceContextMode.PerCall, as shown in bold below:

     [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] public class ShoppingCartService : IShoppingCartService {      }

  3. Start the solution without debugging. In the ShoppingCartClient console window displaying the message “Press ENTER when the service has started,” press Enter.

    The client application adds the three items to the shopping cart as before, but the result displayed after retrieving the shopping cart from the service shows no items and a total cost of zero:

    image from book

    Every time the client application calls the service, it calls a new instance of the service. The shopping cart is destroyed every time an operation completes, so the string returned by the GetShoppingCart operation is a representation of an empty shopping cart.

  4. Press Enter to close the client application console window. In the host application console window, press Enter to stop the service.

  5. In Visual Studio 2005, change the InstanceContextMode property of the ServiceBehavior attribute of the ShoppingCartService to InstanceContextMode.Single, as follows:

     [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)] public class ShoppingCartService : IShoppingCartService {      }

  6. Start the solution without debugging. In the ShoppingCartClient console window press Enter.

    This time, the client application displays the shopping cart containing two water bottles and a mountain seat assembly. All appears to be well at first glance.

  7. Press Enter to close the client application console window, but leave the host application running.

  8. In Visual Studio 2005, right-click the ShoppingCartClient project, point to Debug, and click Start new instance.

    This action runs the client application again without restarting the service host application.

  9. In the ShoppingCartClient console window press Enter.

    The shopping cart displayed by the client application now contains four water bottles and two mountain seat assemblies:

    image from book

    The second run of the client application used the same instance of the service as the first run, and the items were added to the same instance of the shopping cart.

  10. Press Enter to close the client application console window. In the host application console window, press Enter to stop the service.

    Tip 

    The PerSession instance context mode is the default when you use an endpoint that requires sessions. This is actually most of the time, unless you disable security (absolutely not recommended), or use the BasicHttpBinding binding, which does not support sessions when the service host defaults to using the PerCall instance context mode. This can be quite confusing, so it is better to always explicitly state the instance context mode your service requires by using the ServiceBehavior attribute.

Maintaining State with the PerCall Instance Context Mode

The exercises so far in this chapter have highlighted what happens when you change the instance context mode for a service. In the ShoppingCartService service, which instance context mode should you use? In a real-world environment, using a proper client application rather than the test code you have been working with, the user could spend a significant amount of time browsing for items of interest before adding them to their shopping cart. In this case, it makes sense to use the PerCall instance context mode. But you must provide a mechanism to store and recreate the shopping cart each time the client application invokes an operation. There are several ways you can achieve this, including generating an identifier for the shopping cart when the service first creates it, returning this identifier to the client application, and forcing the client to pass this identifier in to all subsequent operations as a parameter. This technique, and its variations, are frequently used, but suffer from many of the same security drawbacks as cookies as far as the service is concerned; it is possible for a client application to forge a shopping cart identifier and hijack another user’s shopping cart.

An alternative strategy is to employ the user’s own identity as a key for saving and retrieving state information. In a secure environment, this information is transmitted as part of the request anyway, and so it is transparent to client applications–for example, the wsHttpBinding binding uses Windows Integrated Security and transmits the user’s credentials to the WCF service by default. You will make use of this information in the following exercise.

Note 

The same mechanism works even if you are using a non-Windows specific mechanism to identify users, such as certificates, and so is a valuable technique in an Internet security environment. The important thing is that you have a unique identifier for the user–it does not have to be a Windows username.

Manage state in the ShoppingCartService service

  1. In Visual Studio 2005, edit the image from book ShoppingCartService.cs file in the ShoppingCartService project.

  2. Add the following using statements to the list at the top of the file:

     using System.IO; using System.Xml.Serialization;

    You will use classes in these namespaces to serialize the user’s shopping cart and save it in a text file.

  3. Modify the definition of the ShoppingCartItem class; mark it with the Serializable attribute, and change its visibility to public, as shown in bold below:

     [Serializable] public class ShoppingCartItem {      }

    You can only serialize publicly accessible classes by using the XML serializer.

  4. Add the saveShoppingCart method shown below to the ShoppingCartServiceImpl class:

     // Save the shopping cart for the current user to a local XML // file named after the user private void saveShoppingCart() {     string userName = ServiceSecurityContext.Current.PrimaryIdentity.Name;     foreach (char badChar in Path.GetInvalidFileNameChars())     {         userName = userName.Replace(badChar, '!');     }     string fileName = userName + ".xml";     TextWriter writer = new StreamWriter(fileName);     XmlSerializer ser = new XmlSerializer(typeof(List<ShoppingCartItem>));     ser.Serialize(writer, shoppingCart);     writer.Close(); }

    Note 

    The code for this method is available in the file image from book SaveShoppingCart.txt located in the Chapter 7 folder.

    This private utility method retrieves the name of the user running the client application and creates a file name based on this username, with an “.xml” suffix. The username could include a domain name, with a separating “\” character. This character is not allowed in file names, so the code replaces any “\” characters, and any other characters in the username that are not allowed in filenames, with a “!” character.

    Note 

    If you are using certificates rather than Window’s usernames to identify users in an Internet environment, the file names will still be legal although they will look a little strange, as user identities in this scheme have the form “CN=user; FA097524718BD8765D6E4AA7654891245BCAD85.”

    The method then uses an XmlSerializer object to serialize the user’s shopping cart to this file before closing the file and finishing.

    Note 

    For clarity, this method does not perform any exception checking. In a production environment, you should be prepared to be more robust.

  5. Add the restoreShoppingCart method shown here to the ShoppingCartServiceImpl class:

     // Restore the shopping cart for the current user from the local XML // file named after the user private void restoreShoppingCart() {     string userName = ServiceSecurityContext.Current.PrimaryIdentity.Name;     foreach (char badChar in Path.GetInvalidFileNameChars())     {         userName = userName.Replace(badChar, '!');     }     string fileName = userName + ".xml";     if (File.Exists(fileName))     {         TextReader reader = new StreamReader(fileName);         XmlSerializer ser =             new XmlSerializer(typeof(List<ShoppingCartItem>));         shoppingCart = (List<ShoppingCartItem>)ser.Deserialize(reader);         reader.Close();     } }

    Note 

    The code for this method is available in the file image from book RestoreShoppingCart.txt located in the Chapter 7 folder.

    This method uses the username to generate a file name using the same strategy as the saveShoppingCart method. If the file exists, this method opens the file and deserializes its contents into the shoppingCart variable before closing it. If there is no such file, the shoppingCart variable is left at its initial value of null.

    Note 

    In a production environment, you should verify that the file contains a valid representation of a shopping cart before attempting to cast its contents and assign it to the shoppingCart variable.

  6. In the AddItemToCart method, call the restoreShoppingCart method before examining the shopping cart, as follows:

     public bool AddItemToCart(string productNumber) {     // Note: For clarity, this method performs very limited security     // checking and exception handling     try     {         // Check to see whether the user has already added this         // product to the shopping cart         restoreShoppingCart();         ShoppingCartItem item = find(shoppingCart, productNumber);          }

  7. In the block of code that increments the volume field of an item, following the if statement, call the saveShoppingCart method to preserve its contents before returning:

     if (item != null) {     item.Volume++;     saveShoppingCart();     return true; }

  8. In the block of code that adds a new item to the shopping cart, call the saveShoppingCart method before returning:

     if (productsReader.Read()) {     …     // Add the new item to the shopping cart     shoppingCart.Add(newItem);     saveShoppingCart();     // Indicate success     return true; }

    There is no need to save the shopping cart whenever the method fails (returns false).

  9. In the RemoveItemFromCart method, call the restoreShoppingCart method before examining the shopping cart, as follows:

     public bool RemoveItemFromCart(string productNumber) {     // Determine whether the specified product has an     // item in the shopping cart     restoreShoppingCart();     ShoppingCartItem item = find(shoppingCart, productNumber);      }

  10. Save the shopping cart after successfully removing the specified item and before returning true:

     // Indicate success saveShoppingCart(); return true;

  11. In the GetShoppingCart method, call the restoreShoppingCart method before iterating through the contents of the shopping cart, as follows:

     public string GetShoppingCart() {     …     restoreShoppingCart();     foreach (ShoppingCartItem item in shoppingCart)     {              } }

  12. Change the InstanceContextMode property of the ServiceBehavior attribute of the ShoppingCartServiceImpl class back to InstanceContextMode.PerCall:

     [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] public class ShoppingCartServiceImpl : IShoppingCartService {      }

    Remember that this instance context mode releases the service instance at the end of each operation.

Test the state management feature of the ShoppingCartService service

  1. Start the solution without debugging. In the ShoppingCartClient console window, press Enter.

    The client application adds the three items to the shopping cart and then displays the contents. The service saves and restores the shopping cart between operations.

  2. Press Enter to close the client application console window. In the host application console window, press Enter to stop the service.

  3. Start the solution again. In the client application console window, press Enter. This time, the client displays a shopping cart containing four water bottles and two mountain seat assemblies. Because the state information is stored in an external file, it persists across service shutdown and restart.

    Note 

    As an additional exercise, you could add some code to the Checkout method to delete the shopping cart file for the user after they have paid for their goods.

  4. Press Enter to close the client application console window. In the host application console window, press Enter to stop the service.

  5. Using Windows Explorer, move to the Chapter 7\ShoppingCartService\ShoppingCartHost\bin\Debug folder. You should see an XML file in this folder called YourDomain!YourName.xml, where YourDomain is the name of your Windows XP computer, or the domain you are a member of, and YourName is your Windows username.

  6. Open this file by using Notepad. It should look like this:

     <?xml version="1.0" encoding="utf-8"?> <ArrayOfShoppingCartItem xmlns:xsi="http://www.w3.org/2001/XMLSchema- instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">   <ShoppingCartItem>     <ProductNumber>WB-H098</ProductNumber>     <ProductName>Water Bottle - 30 oz.</ProductName>     <Cost>4.9900</Cost>     <Volume>4</Volume>   </ShoppingCartItem>   <ShoppingCartItem>     <ProductNumber>SA-M198</ProductNumber>     <ProductName>LL Mountain Seat Assembly</ProductName>     <Cost>133.3400</Cost>     <Volume>2</Volume>   </ShoppingCartItem> </ArrayOfShoppingCartItem>

    Close Notepad and return to Visual Studio 2005.

  7. Return to Visual Studio 2005, and edit the image from book Program.cs file in the ShoppingCartClient project. Add the statements shown in bold below to the Main method, replacing LON-DEV-01 with the name of your domain or computer:

     … // Connect to the ShoppingCartService service ShoppingCartServiceClient proxy =     new ShoppingCartServiceClient("HttpBinding_ShoppingCartService"); // Provide credentials to identify the user proxy.ClientCredentials.Windows.ClientCredential.Domain = "LON-DEV-01"; proxy.ClientCredentials.Windows.ClientCredential.UserName = "Fred"; proxy.ClientCredentials.Windows.ClientCredential.Password = "Pa$$w0rd"; // Add two water bottles to the shopping cart proxy.AddItemToCart("WB-H098");  

    You created the user Fred in Chapter 4, “Protecting an Enterprise WCF Service.”

  8. Start the solution without debugging. In the client application console window, press Enter. The client application displays a shopping cart containing only three items–this is Fred’s shopping cart and not the one created earlier.

  9. Press Enter to close the client application console window. In the host application console window, press Enter to stop the service.

  10. In Windows Explorer, you should see another XML file in Chapter 7\ShoppingCartService\ShoppingCartHost\bin\Debug folder, called YourDomain!Fred.xml.

This solution implements an effective balance between resource use and responsiveness. Although a new service instance has to be created for every operation, and it takes time to restore and save session state, you do not need to retain a service instance in memory for every active client application, so the solution should scale effectively as more and more users access your service.

There are two other points worth making about the sample code in this exercise:

  1. The restoreShoppingCart and saveShoppingCart methods are not currently thread-safe. This might not seem important as the ShoppingCartService uses the PerCall instance context mode and the single-threaded concurrency mode. However, if the same user (such as Fred) runs two concurrent instances of the client application, it will establish two concurrent instances of the service, which will both attempt to read and write the same file. The file access semantics of the .NET Framework class library prevents the two service instances from physically writing to the same file at the same time, but both service instances can still interfere with each other. Specifically, the saveShoppingCart method simply overwrites the XML file, so one instance of the service can obliterate any data saved by the other instance. In a production environment, you should take steps to prevent this situation from occurring, such as using some sort of locking scheme, or maybe using a database rather than a set of XML files.

  2. The saveShoppingCart method creates human-readable XML files. In a production environment, you should arrange for these files to be stored in a secure location rather than the folder holding the service executables. For reasons of privacy, you don’t want other users to be able to access these files or modify them.

Selectively Controlling Service Instance Deactivation

The service instance context mode determines the lifetime of service instances. This property is global across the service; you set it once for the service class, and the WCF runtime handles client application requests and directs them to an appropriate instance of the service (possibly creating a new instance of the service), irrespective of the operations that the client application actually invokes.

The WCF runtime enables you to selectively control when a service instance is deactivated, based on the operations being called. You can tag each method that implements an operation in a service with the OperationBehavior attribute. This attribute has a property called ReleaseInstanceMode that you can use to modify the behavior of the service instance context mode. You use the OperationBehavior attribute like this:

 [OperationBehavior(ReleaseInstanceMode = ReleaseInstanceMode.AfterCall)] public bool Checkout() {     … }

The ReleaseInstanceMode property can take one of these values:

  • ReleaseInstanceMode.AfterCall. When the operation completes, the WCF runtime will release the service instance for recycling. If the client invokes another operation, the WCF runtime will create a new service instance to handle the request.

  • ReleaseInstanceMode.BeforeCall. If a service instance exists for the client application, the WCF runtime will release it for recycling and create a new one for handling the client application request.

  • ReleaseInstanceMode.BeforeAndAfterCall. This is a combination of the previous two values; the WCF runtime creates a new service instance for handling the operation and releases the service instance for recycling when the operation completes.

  • ReleaseInstanceMode.None. This is the default value. The service instance is managed according to the service instance context mode.

You should be aware that you can only use the ReleaseInstanceMode property to reduce the lifetime of a service instance, and you should understand the interplay between the InstanceContextMode property of the ServiceBehavior attribute and the ReleaseInstanceMode property of any OperationBehavior attributes adorning methods in the service class. For example, if you specify an InstanceContextMode value of InstanceContextMode.PerCall and a ReleaseInstanceMode value of ReleaseInstanceMode.BeforeCall for an operation, the WCF runtime will still release the service instance when the operation completes. The semantics of InstanceContextMode.PerCall cause the service to be released at the end of an operation, and the ReleaseInstanceMode property cannot force the WCF runtime to let the service instance live on. On the other hand, if you specify an InstanceContextMode value of InstanceContextMode.Single and a ReleaseInstanceMode value of ReleaseInstanceMode.AfterCall for an operation, the WCF runtime will release the service instance at the end of the operation, destroying any shared resources in the process (there are some threading issues that you should also consider as part of your design if the service is multi-threaded, in this case).

The ReleaseInstanceMode property of the OperationBehavior is most commonly used in conjunction with the PerSession instance context mode. If you need to create a service that uses PerSession instancing, you should carefully assess whether you actually need to hold a service instance for the entire duration of a session. For example, if you know that a client always invokes a particular operation or one of a set of operations at the end of a logical piece of work, you can consider setting the ReleaseInstanceMode property for the operation to ReleaseInstanceMode.AfterCall.

An alternative technique is to make use of some operation properties that you can use to control the sequence of operations in a session, which you will look at next.




Microsoft Windows Communication Foundation Step by Step
Microsoft Windows Communication Foundation Step by Step (Step By Step Developer Series)
ISBN: 0735623368
EAN: 2147483647
Year: 2007
Pages: 105
Authors: John Sharp

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