This post will show you how to maintain State with the PerCall Instance Context Mode. This is the third part of the series, discussing how to manage State and Service instances in WCF services. In the first two posts, we saw how to create a ShoppingCart WCF service where the service side was responsible for keeping the shopping cart state. In the second post, we discuss our options for manage Service Instances in WCF services and how the “PerCall” Instance Context mode, raises several issues that must be solved. If you remember, when changing the Instance Context Mode to “PerCall”, every time an operation call was completed, the shoppingCart list variable was destroyed, not letting us to use it for more calls. This means that we need to provide a mechanism to store and recreate the shopping cart each time the client application invokes an operation. So let’s do it.
I assume you have read the two previous posts, since I am gonna continue modifying the latest version of the project we created.
The plan is this: We are going to Serialize the shopping cart for each client, save it in a text file and retrieving it when asked by the same client. But how do we know which client is the right one? To solve this we will make use of the default “ws2007HttpBinding’s” binding configuration, which uses Windows Integrated Security and transmits the user’s credentials to the WCF service by default. In other words, we will use each user’s identity as a key for saving and retrieving it’s shopping cart state.
In Visual Studio open the IShoppingCartService.cs file and add a using statement to support serializing the “ShoppingCartItem” class. Also, add a [Serializable] attribute above that class and make class public.
using System.Xml.Serialization; namespace ShoppingCartService { // Shopping cart item [Serializable] public class ShoppingCartItem { public string ProductNumber { get; set; } public string ProductName { get; set; } public decimal Cost { get; set; } public int Volume { get; set; } } [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(); } }
Change to the Service implementation file, “ShoppingCartService.cs”, add two using statements for supporting serializing and IO operations. I added a method to serialize and save the shoppingCart list variable, to an .xml file named with the client’s user name. If user’s identity name has invalid characters like “/” (often come from domain – domain/username) I need to replace them with a “!”.
using System.Xml.Serialization; using System.IO; namespace ShoppingCartService { [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)] public class ShoppingCartServiceImpl : IShoppingCartService { 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(); } private List<ShoppingCartItem> shoppingCart = new List<ShoppingCartItem>(); //Other code omitted
Under this private method add a new one, for retrieving the ShoppingCart item list this time. It’s easy to understand what the method is doing. Get user’s name, see if an username.xml file exists and if so, retrieve the shoppingCart by deserializing it.
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(); } }
Now we need to modify our operation implementations to save and retrieve the shoppingCart when necessary. Starting with the “AddItemToCart” add a restoreShoppingCart call before searching the shopping cart (so it sees the latest version of it). Then add a saveShoppingCart call at the time the item volume is incremented (item was found in shopping cart) and a call after an item have been added to the cart (item added for first time).
public bool AddItemToCart(string productNumber) { try { restoreShoppingCart(); ShoppingCartItem item = find(shoppingCart, productNumber); // If so, then simply increment the volume if (item != null) { item.Volume++; saveShoppingCart(); return true; } else { using (AdventureWorks2012Entities database = new AdventureWorks2012Entities()) { // Retrieve the details of the selected product Product product = (from p in database.Products where string.Compare(p.ProductNumber, productNumber) == 0 select p).First(); 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); saveShoppingCart(); // Indicate success return true; } } } catch { // If an error occurs, finish and indicate failure return false; } }
In the same direction add a RestoreShoppingCart() and a SaveShoppingCart() call in the RemoveItemFromCart() operation implementation when necessary. Restore before searching the item and save after removed it.
public bool RemoveItemFromCart(string productNumber) { restoreShoppingCart(); ShoppingCartItem item = find(shoppingCart, productNumber); if (item != null) { item.Volume--; if (item.Volume == 0) { shoppingCart.Remove(item); } saveShoppingCart(); return true; } return false; }
In the GetShoppingCart operation implementation add a RestoreShoppingCart() call before iterating the items in the shopping cart.
public string GetShoppingCart() { string formattedContent = String.Empty; decimal totalCost = 0; restoreShoppingCart(); 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; }
Now it’s time to test our implementation but before doing it, make sure you have selected the “PerCall” Instance Context Mode option for the implementation class.
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] public class ShoppingCartServiceImpl : IShoppingCartService
Build the solution. Create a new proxy class in the same way we did in previous posts. Copy and paste the new “ShoppingCartServiceProxy.cs” file to both of the client’s projects. Make sure you have reserved the http://+:9000/ address to your username account. For these operations you can go and read the previous posts. Run the solution as it is.
Can you see the amazing difference? Despite using the “PerCall” option for service instance, the state of the shopping cart is maintained for the current user. First I pressed enter for the first client who creates two proxys if you recall, and add two kind of items in the cart. Notice that the second proxy has access to the same shopping cart item with the first. This is because WCF service calls are invoked by the same Windows user account, in my case “developer-pc/developer”. The second client project, also has access to that shopping cart since his proxy uses the same Windows User account credentials. So all calls use the same developer-pc!developer.xml file located at the bin/Debug folder of the host project.
Open and see the content of that file. This is the State of the shopping cart variable.
<?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>SA-M687</ProductNumber> <ProductName>HL Mountain Seat Assembly</ProductName> <Cost>196.9200</Cost> <Volume>6</Volume> </ShoppingCartItem> <ShoppingCartItem> <ProductNumber>SA-M198</ProductNumber> <ProductName>LL Mountain Seat Assembly</ProductName> <Cost>133.3400</Cost> <Volume>3</Volume> </ShoppingCartItem> </ArrayOfShoppingCartItem>
You wanna see something more interesting? Try and call the WCF service from the new client project using another windows account. In my case I will modify the proxy credentials of that client to user a user account with Username:Bert and Pass:password. Run the solution. What we expect is the new client use different shopping cart variable, which is restored by a different .xml file named “developer-pc/Bert.xml”. Before running the solution you maybe want to delete the old .xml file so you can have an empty shopping cart.
class Program { 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"); proxy.ClientCredentials.Windows.ClientCredential.UserName = "Bert"; proxy.ClientCredentials.Windows.ClientCredential.Password = "password"; proxy.AddItemToCart("SA-M687"); proxy.AddItemToCart("SA-M687"); // 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("Second client starting.."); 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(); } }
That’s it. I hope you enjoyed it.
Categories: WCF
Hello to every one, because I am actually eager of reading this
web site’s post to be updated on a regular basis. It includes nice stuff.