Maintaining State and Sequencing Operations in Windows Communication Foundation 4

  • 11/23/2010
The issue of sequencing operations should naturally lead you to consider the need to maintain state information between operations. In this chapter from Windows Communication Foundation 4 Step by Step, you will investigate some of these issues and see how you can resolve them.

After completing this chapter, you will be able to:

  • Describe how WCF creates an instance of a service.

  • Explain the different options available for creating service instances.

  • Manage state information in a WCF service in a scalable manner.

  • Fine tune the way in which the WCF runtime manages service instances.

  • Describe how to control the life cycle of a service instance.

  • Describe how to create durable services that can persist their state to a database.

In all the exercises that you have performed so far, the client application has invoked a series of operations in a WCF service. The order of these operations has been immaterial, so calling one operation before another has had no impact on the functionality of either; the operations are totally independent. In the real world, a Web service might require that operations be invoked in a particular sequence. For example, if you are implementing shopping cart functionality in a service, it does not make sense to allow a client application to perform a checkout operation to pay for goods before actually putting anything into the shopping cart.

The issue of sequencing operations should naturally lead you to consider the need to maintain state information between operations. Taking the shopping cart example, where should you store the data that describes the items in the shopping cart? You have at least two options:

  • Maintain the shopping cart in the client application. With this method, you pass the information that describes the shopping cart contents as a parameter to each server-based operation and return the updated shopping cart contents from the operation back to the client. This is a variation of the solution implemented by traditional Web applications (including ASP.NET Web applications) that used cookies stored on the user’s computer to store information. It relieved the Web application of the burden of maintaining state information between client calls, but there was nothing to stop the client application directly modifying the data in the cookie or even inadvertently corrupting it in some manner. Additionally, cookies can be a security risk, and as a result, many Web browsers implement features that let users disable them. This makes it difficult to store state information on the user’s computer. In a Web service environment (as opposed to a Web application and browser combination), a client application can maintain state information using its own code rather than relying on cookies. However, this strategy ties the client application to the Web service and can result in a very tight coupling between the two, with all the inherent fragility and maintenance problems that this can cause.

  • Maintain the shopping cart contents in the service. The first time the user running the client application attempts to add something to the shopping cart, the service creates a data structure to represent the items being added. As the user adds further items to the shopping cart, they are stored in this data structure. When the user wants to pay for the items in the shopping cart, the service can calculate the total, perform an exchange with the user through the client application to establish the payment method, and then arrange for dispatch of the items. In a WCF environment, all interactions between the client application and the service are performed by invoking well-defined operations, specified by using a service contract. Additionally, the client application does not need to know how the service actually implements the shopping cart.

The second approach sounds like the more promising of the two, but there are several issues that you must address when building a Web service to handle this scenario. In this chapter, you will investigate some of these issues and see how you can resolve them.

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 was passed in as a series of parameters by the client application. When the operation completes, 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, although apparently simple, requires a little thought and careful design to work reliably in a scalable manner.

Create the ShoppingCartService Service

  1. Using Visual Studio, create a new project by using the WCF Service Library template in the WCF folder (within the Visual C# folder), in the Installed Templates pane. Specify the following properties for the solution:

    Property

    Value

    Name

    ShoppingCartService

    Location

    Microsoft Press\WCF Step By Step\Chapter 7

    Solution name

    ShoppingCart

  2. In Solution Explorer, rename the IService1.cs file to IShoppingCartService.cs and allow Visual Studio to rename all references to IService1 to IShoppingCartService when prompted.

  3. Change the name of the Service1.cs file to ShoppingCartServce.cs. Again, allow Visual Studio to rename all references to Service1 to ShoppingCartService.

  4. Add a reference to the ProductsEntityModel assembly, which is located in the Microsoft Press\WCF Step By Step\Chapter 7 folder (within your Documents folder). Remember that this assembly contains a copy of the entity model for the Product and Product-Inventory tables in the AdventureWorks database.

  5. Add a reference to the System.Data.Entity assembly. This assembly is required by the ProductsEntityModel assembly.

  6. Open the IShoppingCartService.cs file in the Code And Text Editor window. Delete all the comments and code except the using statements at the top of the file and the ShoppingCartService namespace.

  7. Add the following class (shown in bold) to the ShoppingCartService namespace:

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

    This class defines the items that can be stored in the shopping cart, which will contain a list of these items. Notice that this is not a data contract; this type 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. This way, there should be no dependencies between the structure of the shopping cart and the client applications that manipulate instances of it.

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

    namespace ShoppingCartService
    {
        ...
        [ServiceContract(Namespace = "http://adventure-works.com/2010/06/04",
                         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. The AdventureWorks database identifies items by their product number. To add more than one instance of an item, you must invoke the AddItemToCart operation for each instance. These operations will return true if they are successful or false if otherwise.

    The GetShoppingCart operation returns 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 will return true if it is successful or false if it is not.

  9. Open the ShoppingCartService.cs file in the Code And Text Editor window. As you did with the IShoppingCartService.cs file, delete all the comments and code except the using statements at the start of the file and the ShoppingCartService namespace. Add the following using statement to the list at the top of the file:

    using ProductsEntityModel;
  10. Add the following class to the ShoppingCartService namespace in the ShoppingCart Service.cs file:

    namespace ShoppingCartService
    {
        public class ShoppingCartServiceImpl : IShoppingCartService
        {
        }
    }

    This class will implement the operations for the IShoppingCartService interface.

  11. Add the following private shoppingCart field to the ShoppingCartServiceImpl class:

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

    This variable will hold the user’s shopping cart, which comprises a list of ShoppingCartItem objects. This list represents state information that the service must maintain between calls made by a client application.

  12. Add the following private find method (shown in bold) to the ShoppingCartServiceImpl class:

    public class ShoppingCartServiceImpl : IShoppingCartService
    {
        ...
        // 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;
        }
    }

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

  13. Implement the AddItemToCart method in the ShoppingCartServiceImpl class, as shown in bold in the following:

    public class ShoppingCartServiceImpl : IShoppingCartService
    {
        ...
        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, then simply 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
                    // by using the Entity Framework
                    using (AdventureWorksEntities database = new AdventureWorksEntities())
                    {
                        // Retrieve the details of the selected product
                        Product product = (from p in database.Products
                                           where string.Compare(p.ProductNumber,
                                               productNumber) == 0
                                           select p).First();
    
                        // Create and populate a new shopping cart item
                        ShoppingCartItem newItem = new ShoppingCartItem
                        {
                            ProductNumber = product.ProductNumber,
                            ProductName = product.Name,
                            Cost = product.ListPrice,
                            Volume = 1
                        };
    
                        // Add the new item to the shopping cart
                        shoppingCart.Add(newItem);
    
                        // Indicate success
                        return true;
                    }
                }
            }
            catch
            {
                // If an error occurs, finish and indicate failure
                return false;
            }
        }
    }
  14. Add the following RemoveItemFromCart method (shown in bold) to the ShoppingCartServiceImpl class:

    public class ShoppingCartServiceImpl : IShoppingCartService
    {
        ...
        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;
        }
    }
  15. Implement the GetShoppingCart method in the ShoppingCartServiceImpl class, as follows:

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

    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.

  16. Add the Checkout method to the ShoppingCartServiceImpl class, as shown in bold in the following:

    public class ShoppingCartServiceImpl : IShoppingCartService
    {
        ...
        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.

  17. 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. Specify the following properties for the project:

    Property

    Value

    Template

    Console Application, in the Windows folder, within the Visual C# folder, in the Installed Templates pane

    Name

    ShoppingCartHost

    Location

    Microsoft Press\WCF Step By Step\Chapter 7\ShoppingCart

  2. Add a reference to the ShoppingCartService project for the ShoppingCartHost project. Also add references to the System.ServiceModel and System.Data.Entity assemblies.

  3. Add the App.config file located in the Microsoft Press\WCF Step By Step\Chapter 7 folder to the ShoppingCartHost project.

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

  4. Edit the App.config file for the ShoppingCartHost project by using the Service Configuration Editor. In the Services pane, click Create A New Service. Proceed through the New Service Element Wizard, using the information in the following table to define the service.

    Page

    Prompt

    Value

    What is the service type of your service?

    Service type

    ShoppingCartService.ShoppingCartServiceImpl

    What service contract are you using?

    Contract

    ShoppingCartService.IShoppingCartService

    What communications mode is your service using?

    HTTP

    What method of interoperability do you want to use?

    Advanced Web Service interoperability (Simplex communication)

    What is the address of your endpoint?

    Address

    http://localhost:9000/ShoppingCartService/ShoppingCartService.svc

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

  5. Save the configuration file, and then exit the Service Configuration Editor.

  6. Open the App.config file for the ShoppingCartHost project in the Code And Text Editor window. The <system.serviceModel> section should look like this:

    <system.serviceModel>
      <services>
        <service name="ShoppingCartService.ShoppingCartServiceImpl">
          <endpoint
            address= "http://localhost:9000/ShoppingCartService/ShoppingCartService.svc"
            binding="ws2007HttpBinding" bindingConfiguration=""
            contract="ShoppingCartService.IShoppingCartService" />
        </service>
      </services>
    </system.serviceModel>
  7. Open the Program.cs file for the ShoppingCartHost project in the Code And Text Editor window. Add the following using statement to the list at the top of the file:

    using System.ServiceModel;
  8. Add the following statements (shown 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. Specify the following properties for the project:

    Property

    Value

    Template

    Console Application, in the Windows folder, within the Visual C# folder, in the Installed Templates pane

    Name

    ShoppingCartClient

    Location

    Microsoft Press\WCF Step By Step\Chapter 7\ShoppingCart

  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:

    1. Open a Visual Studio Command Prompt window and move to the ShoppingCart\ShoppingCartService\bin\Debug folder in the Microsoft Press\WCF Step By Step\Chapter 7 folder.

    2. In the Visual Studio Command Prompt window, run the command:

      svcutil ShoppingCartService.dll
    3. Run the command:

      svcutil /namespace:*,ShoppingCartClient.ShoppingCartService
          adventure-works.com.2010.06.04.wsdl *.xsd /out:ShoppingCartServiceProxy.cs
  4. Close the Visual Studio Command Prompt window and return to Visual Studio. Add the ShoppingCartServiceProxy.cs file in the ShoppingCart\ShoppingCartService\bin\Debug folder to the ShoppingCartClient project.

  5. Add a new application configuration file to the ShoppingCartClient project. Name this file App.config.

  6. Edit the App.config file in the ShoppingCartClient project by using the Service Configuration Editor. In the Configuration pane, click the Client folder. In the Client pane, click Create A New Client. Use the New Client Element Wizard to add a new client endpoint to the configuration file by using the information in the following table:

    Page

    Prompt

    Value

    What method do you want to use to create the client?

    From service config

    Microsoft Press\WCF Step By Step\Chapter 7\ShoppingCart\ShoppingCartHost\App.config

    Which service endpoint do you want to connect to?

    Service endpoint

    ShoppingCartService.ShoppingCartServiceImpl-

    http://localhost:9000/ShoppingCart Service/ShoppingCartService.svc-

    ws2007HttpBinding-

    ShoppingCartService.IShoppingCart Service

    Note: This is the default endpoint.

    What name do you want to use for the client configuration?

    WS2007HttpBinding_IShoppingCart Service

    The wizard adds the client definition to the configuration file and creates an endpoint called WS2007HttpBinding_IShoppingCartService 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.

  7. In the Configuration pane, in the Endpoints folder under the Client folder, click the WS2007HttpBinding_IShoppingCartService endpoint. In the Client Endpoint pane, set the Contract property to ShoppingCartClient.ShoppingCartService.ShoppingCartService (the type is ShoppingCartService in the ShoppingCartClient.ShoppingCartService namespace in the client proxy).

  8. Save the configuration file and exit the Service Configuration Editor. Allow Visual Studio to reload the modified App.config file, if prompted.

  9. Open the App.config file for the ShoppingCartClient application in the Code And Text Editor window. It should appear as follows:

    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
      <system.serviceModel>
        <client>
          <endpoint address="http://localhost:9000/ShoppingCartService/
              ShoppingCartService.svc"
              binding="ws2007HttpBinding" bindingConfiguration=""
              contract="ShoppingCartClient.ShoppingCartService.ShoppingCartService"
              name="WS2007HttpBinding_IShoppingCartService" kind=""
              endpointConfiguration="">
              <identity>
                <certificateReference storeName="My" storeLocation="LocalMachine"
                    x509FindType="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.

  10. In Visual Studio, open the Program.cs file for the ShoppingCartClient project in the Code And Text Editor window. Add the following using statements to the list at the top of the file.

    using System.ServiceModel;
    using ShoppingCartClient.ShoppingCartService;
  11. Add the following statements (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("WS2007HttpBinding_IShoppingCartService");
    
            // 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();
    }

    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.

  12. Open a Visual Studio Command Prompt window as an administrator and enter the following command to reserve port 9000 for your service (replace UserName with your Windows user name):

    netsh http add urlacl url=http://+:9000/ user=UserName
  13. Close the Visual Studio Command Prompt window and return to Visual Studio. In Solution Explorer set the ShoppingCartClient and ShoppingCartHost projects as the startup projects for the solution.

  14. Start the solution without debugging. In the client 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 and displays the result, as shown in the following image (your currency symbol might be different if you are not in the United Kingdom):

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

You can see that the ShoppingCartService service has maintained information about the shopping cart for the client application between calls, so this technique for maintaining state information in the service appears to work well. But, this is one of those situations that should leave you feeling a little bit suspicious—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!

An instance of a WCF service that is created to handle requests from a specific client application and maintain state information between requests from that client application is called a Session. To be explicit, when a client application uses a proxy object to connect to a service, the WCF runtime for the service host creates a session to hold an instance of the service and any state data required by that instance. The session is terminated when the client application closes the proxy object.

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, as follows:

[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. 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 that property to specify how to process concurrent requests in the same session, as shown in the following:

[ServiceBehavior(..., ConcurrencyMode = ConcurrencyMode.Single)]
public class ShoppingCartServiceImpl : 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 ConcurrencyMode.Multiple does not make any guarantees about synchronization. You must take responsibility for ensuring that the code you write in the service is thread-safe.

There is a third mode called ConcurrencyMode.Reentrant. In this mode, the service instance is single-threaded, but it 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 must hold 10,000 instances of the service, even if 9,999 of them are not currently performing any operations (perhaps 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 because a client application must identify itself so that the service can retrieve the appropriate state from storage (you will investigate a couple of ways of achieving this later in this chapter; a more comprehensive approach is described in Chapter 8, "Implementing Services by Using Workflows").

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; 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 .NET Framework Common Language Runtime (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 expecting the same instance 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 you should 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.

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, edit the ShoppingCartService.cs file in the ShoppingCartService project.

  2. Add the ServiceBehavior attribute to the ShoppingCartServiceImpl class. Set the InstanceContextMode property to InstanceContextMode.PerCall, as shown in bold in the following:

    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
    public class ShoppingCartServiceImpl : IShoppingCartService
    {
        ...
    }
  3. Start the solution without debugging. In the ShoppingCartClient console window that is 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.

    Every time the client application calls the service, it creates a new instance of the service. The shopping cart is destroyed each 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, change the InstanceContextMode property of the ServiceBehavior attribute of the ShoppingCartService to InstanceContextMode.Single, as follows:

    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
    public class ShoppingCartServiceImpl : IShoppingCartService
    {
        ...
    }
  6. Start the solution again, 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 service host application running.

  8. In Solution Explorer, right-click the ShoppingCartClient project, point to Debug, and then 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:

    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.

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 ws2007HttpBinding 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.

Maintain State in the ShoppingCartService Service

  1. In Visual Studio, open the IShoppingCartService.cs file for the ShoppingCartService project in the Code And Text Editor window.

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

    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 in the following:

    [Serializable]
    public class ShoppingCartItem
    {
        ...
    }

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

  4. Open the ShoppingCartService.cs file in the Code And Text Editor window. Add the following using statements to the list at the top of the file:

    using System.Xml.Serialization;
    using System.IO;
  5. Add the private saveShoppingCart method (shown in bold in the following code) to the ShoppingCartServiceImpl class:

    public class ShoppingCartServiceImpl : IShoppingCartService
    {
        ...
        // 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();
        }
        ...
    }

    This private utility method retrieves the name of the user running the client application and creates a file name based on this user name, with the “.xml” file extension. The user name 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 user name that are not allowed in filenames—with a “!” character.

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

  6. Add the following private restoreShoppingCart method (shown in bold) to the ShoppingCartServiceImpl class:

    public class ShoppingCartServiceImpl : IShoppingCartService
    {
        ...
        // 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();
            }
        }
        ...
    }

    This method uses the user name 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.

  7. In the AddItemToCart method, call the restoreShoppingCart method before examining the shopping cart, as follows in bold:

    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);
            ...
        }
        ...
    }
  8. 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:

    public bool AddItemToCart(string productNumber)
    {
        // Note: For clarity, this method performs very limited security
        // checking and exception handling
        try
        {
            ...
            if (item != null)
            {
                item.Volume++;
                saveShoppingCart();
                return true;
            }
            ...
        }
        ...
    }
  9. In the block of code that adds a new item to the shopping cart, call the saveShoppingCart method before returning, as shown in bold in the following:

    public bool AddItemToCart(string productNumber)
    {
        // Note: For clarity, this method performs very limited security
        // checking and exception handling
        try
        {
            ...
            else
            {
                ...
                using (AdventureWorksEntities database = new AdventureWorksEntities())
                {
                    ...
                    // 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).

  10. 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);
        ...
    }
  11. Add the following code (shown in bold) to save the shopping cart after successfully removing the specified item and before returning true:

    public bool RemoveItemFromCart(string productNumber)
    {
        ...
        // Indicate success
        saveShoppingCart();
        return true;
    }
  12. In the GetShoppingCart method, call the restoreShoppingCart method before iterating through the contents of the shopping cart, as shown in bold in the following:

    public string GetShoppingCart()
    {
        ...
        restoreShoppingCart();
        foreach (ShoppingCartItem item in shoppingCart)
        {
            ...
        }
    }
  13. 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 and destroys any state information held in the memory of the service instance. Hopefully, the state of the user’s shopping cart should be persisted to disk. You will test that this is the case in the next exercise.

Test the State Management Capabilities of the ShoppingCartService Service

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

    The client application adds three items to the shopping cart and then displays the contents. The service saves the data in the user’s shopping cart to a file 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 ShoppingCartClient 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.

  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\ShoppingCart\ShoppingCartHost\bin\Debug folder. You should see an XML file in this folder called YourDomain!YourName.xml, where YourDomain is either the name of your computer or the name of the domain of which you are a member, and YourName is your Windows user name.

  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>

    This is the data from your shopping cart. Remember that the saveShoppingCart method writes the data by using an XmlSerializer object to save it as an XML document.

  7. Close Notepad and return to Visual Studio. Edit the Program.cs file in the Shopping CartClient project by adding the following statements (shown in bold) to the Main method (replace Domain with the name of your domain or computer):

    static void Main(string[] args)
    {
        ...
        try
        {
            // Connect to the ShoppingCartService service
            ShoppingCartServiceClient proxy =
                new ShoppingCartServiceClient("WS2007HttpBinding_ShoppingCartService");
    
            // Provide credentials to identify the user
            proxy.ClientCredentials.Windows.ClientCredential.Domain = "Domain";
            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");
            ...
        }
        ...
    }
  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 the Chapter 7\Shopping CartService\ShoppingCartHost\bin\Debug folder, called YourDomain!Fred.xml.

This solution implements a balance between resource use and responsiveness. Although a new service instance must 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 three 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 service 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 other than the folder where the service executables reside. For reasons of privacy, you don’t want other users to be able to access these files or modify them.

  3. The solution relies on users being authenticated and having unique identifiers; they cannot be anonymous. Without authentication and identification, there is no primary identity for each user and the ShoppingCartService service will not be able to generate unique names for files holding the state for each user’s session.

You will revisit these issues later in this chapter and see how you can resolve them in a scalable manner by using a durable service and defining durable operations. Before that, however, it is worth looking at some other features of services and how you can control the sequence of operations that a client application performs. This too can have a bearing on the way in which you maintain state information.

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.

With the WCF runtime, you can 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 attribute 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.