Introduction
This article explains how to develop a stateful WCF service leveraging the ASP.NET infrastructure, i.e., the ASP.NET HTTP Pipeline, using BasicHttpBinding, using an example of a miniature shopping cart.
Background
The background knowledge expected is C#, ASP.NET, and WCF.
Using the Code
We shall follow a contract-first approach quite similar to an SOA implementation for this example.
- Create a blank solution named StateManagementWCF.
- Add a Class Library project named OrderServiceContract.
- Rename Class1.cs to IOrderService.cs and insert the following code therein:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Description;
using System.ServiceModel.Web;
using System.Text;
namespace OrderServiceContract
{
[ServiceContract(Namespace = "OrderServiceContract",
SessionMode = SessionMode.Allowed)]
public interface IOrderService
{
[OperationContract]
void StartPurchase();
[OperationContract]
string PlaceOrder(Item item);
[OperationContract]
string MakePayment(decimal amount);
[OperationContract]
string ShipOrder(string address);
[OperationContract]
void EndPurchase();
}
[DataContract]
public class Item
{
[DataMember]
public string ItemName { get; set; }
[DataMember]
public decimal Price { get; set; }
}
}
Here, you first declare and define a service contract with SessionMode
as Allowed, and then a data contract for the composite object item. With the StartPurchase
operation, the buyer shall start the purchasing session. With the PlaceOrder
operation, the buyer shall place order for an item. This operation can be called multiple times in the session to purchase multiple items. In the service instance itself, the total outstanding amount for the items purchased would be calculated and remembered. Then the buyer would make payments by calling the MakePayment
operation. The cumulative payment made shall be remembered by the service instance each time. Finally, when the buyer calls ShipOrder
, the service instance checks whether full payment has been made or not. In the end, the session is terminated by a call to the EndPurchase
operation. The schema supports session with the help of the SessionMode
attribute.
- Now add another project of type WCF Service Application and name it as UsageService. Add a reference to the System.ServiceModel assembly. Rename the service to OrderService.svc.
- Have the following code in the OrderService.svc.cs file:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Text;
namespace UsageService
{
[AspNetCompatibilityRequirements(RequirementsMode =
AspNetCompatibilityRequirementsMode.Allowed)]
public class OrderService : OrderServiceContract.IOrderService, IDisposable
{
private decimal TotalAmount { get; set; }
private decimal PaymentReceived { get; set; }
private bool TransactionStarted {get; set;}
public OrderService()
{
TotalAmount = 0;
PaymentReceived = 0;
TransactionStarted = false;
}
public void StartPurchase()
{
HttpContext.Current.Session["TransactionStarted"] = true;
HttpContext.Current.Session["TotalAmount"] = 0;
HttpContext.Current.Session["PaymentReceived"] = 0;
}
public string PlaceOrder(OrderServiceContract.Item item)
{
if (Convert.ToBoolean(HttpContext.Current.Session["TransactionStarted"]))
{
HttpContext.Current.Session["TotalAmount"] =
Convert.ToDecimal(
HttpContext.Current.Session["TotalAmount"]) + item.Price;
return "Order placed for item " + item.ItemName +
" and total outstanding amount is $" +
HttpContext.Current.Session["TotalAmount"].ToString();
}
return "Shopping session not yet started";
}
public string MakePayment(decimal amount)
{
if (Convert.ToBoolean(HttpContext.Current.Session["TransactionStarted"]))
{
HttpContext.Current.Session["PaymentReceived"] =
Convert.ToDecimal(
HttpContext.Current.Session["PaymentReceived"]) + amount;
return "Payment made of amount USD " +
HttpContext.Current.Session["PaymentReceived"].ToString() +
" and amount remaining to be paid is $" +
((Convert.ToDecimal(HttpContext.Current.Session["TotalAmount"])) -
(Convert.ToDecimal(
HttpContext.Current.Session["PaymentReceived"]))).ToString();
}
return "Shopping session not yet started";
}
public string ShipOrder(string address)
{
if (Convert.ToBoolean(HttpContext.Current.Session["TransactionStarted"]))
{
if ((Convert.ToDecimal(HttpContext.Current.Session["TotalAmount"])) <=
(Convert.ToDecimal(HttpContext.Current.Session["PaymentReceived"])))
{
return "Ordered items would be reaching" +
" at your doorstep soon. Thanks";
}
return "Please pay the full amount in advance in order to enable " +
"us ship your items, the outstanding amount is $" +
((Convert.ToDecimal(HttpContext.Current.Session["TotalAmount"])) -
(Convert.ToDecimal(
HttpContext.Current.Session["PaymentReceived"]))).ToString();
}
return "Shopping session not yet started";
}
public void EndPurchase()
{
if (Convert.ToBoolean(HttpContext.Current.Session["TransactionStarted"]))
{
HttpContext.Current.Session["TransactionStarted"] = false;
}
}
public void Dispose()
{
}
}
}
Here, the OrderService
class implements the IOrderService
and IDisposable
interfaces. The class is decorated with the attribute:
[AspNetCompatibilityRequirements(RequirementsMode =
AspNetCompatibilityRequirementsMode.Allowed)]
This makes the WCF service leverage the existing ASP.NET HTTP pipeline and thereby get a sort of "license" to use the HttpContext.Current.Session
object to store (remember) stateful data. During the StartPurchase
operation, we initialize the data in session variables, thereby setting up the session. In every other operation, we check whether the session is started or not by checking the TransactionStarted
boolean variable which is stored in a session variable (and therefore remembered across calls).
- Now we shall modify the web.config. The modified web.config is presented below:
="1.0"="UTF-8"
<configuration>
<system.web>
<compilation debug="true"/>
<sessionState cookieless="false" mode="InProc"/>
</system.web>
<system.serviceModel>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
<services>
<service name="UsageService.OrderService"
behaviorConfiguration="UsageService.OrderServiceBehavior">
<host>
<baseAddresses>
<add baseAddress="http://localhost/UsageService" />
</baseAddresses>
</host>
<endpoint address="" binding="basicHttpBinding"
bindingConfiguration="OrderBinding"
contract="OrderServiceContract.IOrderService" />
<endpoint address="mex" binding="mexHttpBinding"
contract="IMetadataExchange" />
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="UsageService.OrderServiceBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true"/>
</behavior>
</serviceBehaviors>
</behaviors>
<bindings>
<basicHttpBinding>
<binding name="OrderBinding" allowCookies="true">
<security mode="None" />
</binding>
</basicHttpBinding>
</bindings>
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true" />
<directoryBrowse enabled="true" />
</system.webServer>
</configuration>
Here, the first thing we do is set the sessionState
element's cookieless
attribute to false
, thereby supporting session cookies and specifying how the state would be maintained. We choose InProc
(in-memory session data store) for now just for simplicity sake, as otherwise we would get diverted from our actual discussion to state management techniques in ASP.NET.
<system.web>
<compilation debug="true"/>
<sessionState cookieless="false" mode="InProc"/>
</system.web>
Next, we set the ASP.NET compatibility as follows:
<system.serviceModel>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
...
...
Here, we enable the aspNetCompatibilityEnabled
attribute of serviceHostingEnvironment
. So IIS hosts the service, and IIS and the ASP.NET integrated modules and handlers provide the hosting environment. Next, we can observe that we have used basicHttpBinding
here, which actually mimics the erstwhile ASP.NET Web Services. Now, in the binding configuration, we allow cookies:
<bindings>
<basicHttpBinding>
<binding name="OrderBinding" allowCookies="true">
<security mode="None" />
</binding>
</basicHttpBinding>
</bindings>
The reason for doing this is that when the client first makes a call to the service through a proxy, a service instance gets created, a session is started, and a session cookie is returned to the client, which the client has to submit to the server with each successive service call in the session. In the session cookie, the session ID is stored, which when the service receives with each successive service call, it understands which client is calling it and what the session data are for that particular client session, given that there might be many such clients calling those service operations simultaneously. The session ID is unique for each session and distinct for all clients.
- Next, we create a virtual directory called UsageService in IIS and test the service by browsing OrderService.svc.
- Next, we add a Windows Forms Application project to the solution named as ShoppingClient, and add a reference to System.ServiceModel to it.
- Now we add a service reference to the ShoppingClient for the endpoint: http://localhost/UsageService/OrderService.svc.
- We then design the UI for the form, and finally add the following code in ShoppingForm.cs:
public partial class ShoppingForm : Form
{
private List<OrderServiceReference.Item> itemList = null;
private OrderServiceReference.OrderServiceClient clientService = null;
public ShoppingForm()
{
InitializeComponent();
itemList = new List<OrderServiceReference.Item>();
}
private void AddItemsToList()
{
OrderServiceReference.Item itm = new OrderServiceReference.Item()
{ ItemName = "Bag", Price = (decimal)10.70 };
itemList.Add(itm);
itm = new OrderServiceReference.Item()
{ ItemName = "Boot", Price = (decimal)11.30 };
itemList.Add(itm);
itm = new OrderServiceReference.Item()
{ ItemName = "Basket", Price = (decimal)10.00 };
itemList.Add(itm);
itm = new OrderServiceReference.Item()
{ ItemName = "Box", Price = (decimal)20.07 };
itemList.Add(itm);
itm = new OrderServiceReference.Item()
{ ItemName = "Bat", Price = (decimal)1.93 };
itemList.Add(itm);
}
private void ShoppingForm_Load(object sender, EventArgs e)
{
try
{
clientService = new OrderServiceReference.OrderServiceClient();
clientService.StartPurchase();
this.AddItemsToList();
}
catch (Exception ex)
{
txtMessage.Clear();
txtMessage.Text = ex.Message + "\n" + ex.Source + "\n" +
ex.StackTrace + "\n" + ex.TargetSite;
}
}
private void btnExit_Click(object sender, EventArgs e)
{
Application.Exit();
}
private void btnMakePayment_Click(object sender, EventArgs e)
{
txtMessage.Clear();
if (txtAmount.Text == String.Empty)
{
MessageBox.Show("Please enter amount first");
return;
}
txtMessage.Text = clientService.MakePayment(
Convert.ToDecimal(txtAmount.Text.Trim()));
txtAmount.Clear();
}
private void btnPurchaseBag_Click(object sender, EventArgs e)
{
txtMessage.Clear();
txtMessage.Text = clientService.PlaceOrder(itemList[0]);
}
private void btnPurchaseBoot_Click(object sender, EventArgs e)
{
txtMessage.Clear();
txtMessage.Text = clientService.PlaceOrder(itemList[1]);
}
private void btnPurchaseBasket_Click(object sender, EventArgs e)
{
txtMessage.Clear();
txtMessage.Text = clientService.PlaceOrder(itemList[2]);
}
private void btnPurchaseBox_Click(object sender, EventArgs e)
{
txtMessage.Clear();
txtMessage.Text = clientService.PlaceOrder(itemList[3]);
}
private void btnPurchaseBat_Click(object sender, EventArgs e)
{
txtMessage.Clear();
txtMessage.Text = clientService.PlaceOrder(itemList[4]);
}
private void btnShipOrder_Click(object sender, EventArgs e)
{
txtMessage.Clear();
txtMessage.Text = clientService.ShipOrder("DF-I, B-2/4, " +
"PURBA ABASAN, 1582/1 RAJDANGA MAIN ROAD, KOLKATA - 700107, INDIA");
}
}
Here, we maintain a list for items ordered and fill the same in a method called AddItemsToList()
, just to keep things simple. Now, during Form_Load
, we call the StartPurchasing()
operation to start the session with the service. We call the PlaeOrder(item)
method to place orders, and the MakePayment(amount)
method to make payments, and finally the ShipOrder(address)
method to request the service to ship the ordered items. We see that we are doing nothing about state management at the client code, because the trick is done in the app.config.
- The App.config file (after modification) is presented as follows:
="1.0"="utf-8"
<configuration>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_IOrderService"
closeTimeout="00:01:00"
openTimeout="00:01:00" receiveTimeout="00:10:00"
sendTimeout="00:01:00"
allowCookies="true" bypassProxyOnLocal="false"
hostNameComparisonMode="StrongWildcard"
maxBufferSize="65536" maxBufferPoolSize="524288"
maxReceivedMessageSize="65536"
messageEncoding="Text" textEncoding="utf-8"
transferMode="Buffered"
useDefaultWebProxy="true">
<readerQuotas maxDepth="32" maxStringContentLength="8192"
maxArrayLength="16384"
maxBytesPerRead="4096" maxNameTableCharCount="16384" />
<security mode="None">
<transport clientCredentialType="None"
proxyCredentialType="None" realm="" />
<message clientCredentialType="UserName"
algorithmSuite="Default" />
</security>
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint address="http://kummu-pc/UsageService/OrderService.svc"
binding="basicHttpBinding"
bindingConfiguration="BasicHttpBinding_IOrderService"
contract="OrderServiceReference.IOrderService"
name="BasicHttpBinding_IOrderService" />
</client>
</system.serviceModel>
</configuration>
Here we have set allowCookies
to true
for the binding. The proxy automatically handles the session cookie returned by the service and submits the session cookie to the service each time a service call is made thereafter. All this plumbing is done transparent to the developer.
Points of Interest
Now, in order to improve the scalability of the application, the session state in web.config should be stored in SQL Server by setting mode="SqlServer"
instead of mode = "InProc"
. The database that gets created automatically is ASPNETDB. Another attribute to be specified when mode="SqlServer"
is the connectionString
. Normally, for high availability, the SQL Servers are active-passive clustered and hence, the session data is not lost due to any failures. Also, transport level security could be implemented using HTTPS instead of HTTP. In that case, a certificate could be built using the makecert utility and placed in a certificate store with public key distributed to the clients. At the message level, encoding could be done to have message level security. Also, last but not the least, the credentials can be configured for basicHttpBinding
and that can leverage the Membership API and Roles API of the ASP.NET HTTP pipeline processing features.
History