Monday, April 6, 2009

Using Session State in a Web Service

Usually, when you think of a Web Service, you think …make the call, get the response, and get on with the task at hand. These "one shot" calls are the norm in Web Services but there may be times when you need a little more. You may need the Web Service to remember states between calls.


As an example, I wrote a Web Service that had to perform a lengthy operation. I didn't want to lock up the client by making a synchronous call, so once the job was started, the call returned. Every few seconds, on a timer, the client would call a GetStatus function in the Web Service to get the status of the running job. When the status was retrieved, a progress bar on the client was updated. It worked well.


There is support for using Session State in Web Services but it doesn't work "out-of-the-box".


This article assumes you know how to create and call a Web Service.


First we need a Web Service. I created a basic Web Service and then modified the supplied HelloWorld function that is created by default:


using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Services;
 
namespace DemoWebService
{
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [System.ComponentModel.ToolboxItem(false)]
 
    public class MyDemo : System.Web.Services.WebService
    {
        [WebMethod (EnableSession = true)]
        public string HelloWorld()
        {
            // get the Count out of Session State
            int? Count = (int?)Session["Count"];
 
            if (Count == null)
                Count = 0;
 
            // increment and store the count
            Count++;
            Session["Count"] = Count;
 
            return "Hello World - Call Number: " + Count.ToString();
        }
    }
}


Note: The key thing to do is add (EnableSession = True) to the WebMethod tag. The rest of the code simple keeps track of how many times each client has called the Web Service method.


Side Note: I am using a Nullable Type to simplify the retrieving of Count from the Session collection.


Here's the old school way: 





int Count;
 
if (Session["Count"] == null)
    Count = 0;
else
    Count = (int)Session["Count"];


Here's the new school way...Nullable Type…is better, yah? 



int? Count = (int?)Session["Count"];
 
if (Count == null)
    Count = 0;


Ok, now the client. You've built the Web Service. You've added the Web Service to the client project (right-click the project and select "Add Web Reference…"). A proxy class has been created.


In a button click event, we instantiate the proxy class, call our function and update a label. This is going to be great…it's going to be so easy…


protected void Button1_Click(object sender, EventArgs e)
{
    // instantiate the proxy and call our function
    localhost.MyDemo MyService = new localhost.MyDemo();
 
    Label1.Text = MyService.HelloWorld();
}
We click the button a few times, but instead counting, we get this: 


Hello World - Call Number: 1
Hello World - Call Number: 1
Hello World - Call Number: 1


Explanation: When an object is put into the Session collection, Asp.Net gives the caller an indentifying cookie behind the scenes…the SessionID. This is like when you check your coat at an expensive restaurant (I've heard) and you get a coat check ticket. When you go to get your coat back, you must have the ticket. So why isn't the proxy storing the SessionID as a cookie?


Key Point: The proxy generated doesn't have a CookieContainer.


Solution: Create a CookieContainer in the proxy (it already has a reference for it)


Here's the modified code:

protected void Button1_Click(object sender, EventArgs e)
{
    // instantiate the proxy 
    localhost.MyDemo MyService = new localhost.MyDemo();
 
    // create a container for the SessionID cookie
    MyService.CookieContainer = new CookieContainer();
 
    // call the Web Service function
    Label1.Text += MyService.HelloWorld() + "<br />";
}


All right, problem solved. This is great…it's going to be so easy… Fire up the web page, click the button a few times, and…

Hello World - Call Number: 1
Hello World - Call Number: 1
Hello World - Call Number: 1


@#%* Why does life have to be so hard?

Explanation: When the proxy went out of scope, it was uninstantiated and ceased to exist. The CookieContainer also ceased to exist. And our cookie, the SessionID…a.k.a. coat check ticket…was lost.


Key Point: The proxy has to remain in existence between calls to the Web Service.


Solution: Keep the proxy around. In a Windows Forms application this is easy. Make the proxy global or static…just don't put it on the stack. For a Web Application it's a bit trickier, we put the proxy into the Session collection.



protected void Button1_Click(object sender, EventArgs e)
{
    localhost.MyDemo MyService;
 
    // try to get the proxy from Session state
    MyService = Session["MyService"] as localhost.MyDemo;
 
    if (MyService == null)
    {
        // create the proxy
        MyService = new localhost.MyDemo();
 
        // create a container for the SessionID cookie
        MyService.CookieContainer = new CookieContainer();
 
        // store it in Session for next usage
        Session["MyService"] = MyService;
    }
 
    // call the Web Service function
    Label1.Text += MyService.HelloWorld() + "<br />";
}  


All right, we hope the problem is solved because this is getting tiring. Fire up the web page, click the button a few times and…


Hello World - Call Number: 1
Hello World - Call Number: 2
Hello World - Call Number: 3


Ah, the sweet bliss of working code.

No comments:

Post a Comment