Friday, October 21, 2011

ASP.NET Web Services and Sessions


By default, Web Services are stateless. However, that doesn't mean that they cannot maintain state.Although you are often told that a statefull Web Service is a Bad Thing, it doesn't hurt to know that the possibility exists.In this article, Bob Swart shows how to add state management and session support to your web services, explaining what to do and what not to do, so you can decide for yourself if, where and when you want to use this ability.
Although there are many development environments that support ASP.NET web services, I want to focus on the issue at hand: adding and using session support in ASP.NET web services.And in order not to bind the steps to a particular IDE, I've decided to use notepad to enter plain C# code used in this article.
The Need For a Session?
The fact that a stateless Web Service is a good thing is explained by the fact that maintaining state requires effort: both in time and memory.Your Web Service will be slower and using more memory than a stateless Web Service.However, there are cases where you may feel you really need to add session support to your Web Services.As an example let's consider a simple guessing game Web Service, where the client can guess a number between 1 and 36, and the Web Service tells you if your guess is too high, too low or just right.This game can be implemented as a Web Service without the need to maintain state (I'll get back to that at the end of this article), but it's still a good example to illustrate how to maintain state and support sessions.
The initial Web Service definition of the GuessWebService consists of three methods: NewGame, Guess and Guesses.If you call the NewGame method, then a new random number is picker, and the guess count is reset to 0.The method Guess can be used to guess (pass) a number, returning 1 if the number is too high, -1 if the number is too low, or 0 if the number if right.Finally, the Guesses method returns the number of guesses made so far.The C# implementation of the game engine can be seen below:
<%@ WebService Language="C#" Class="Game.GuessWebService" %>

using System;
using System.Web.Services;

namespace Game
{
[WebService(Namespace="http://www.eBob42.com")]
public class GuessWebService: System.Web.Services.WebService
{
int SessionNumber;
int SessionGuess;

[WebMethod]
public void NewGame()
{
SessionNumber = (new Random()).Next(0, 36);
SessionGuess = 0;
}

[WebMethod]
public int Guess(int Number)
{
SessionGuess++;
if (Number < SessionNumber) return -1;
else
if
(Number > SessionNumber) return 1;
else
{
return 0;
}
}

[WebMethod]
public int Guesses()
{
return SessionGuess;
}
}
}
The logic is all here: in the NewGame method you can assign values to the SessionNumber and SessionGuess fields, and you use them again in the Guess method.The only downside is that a Web Service is stateless, so no matter what you assign in the NewGame method, the values of SessionNumber and SessionGuess will be empty when you get into the Guess method.And as a side-effect, a guess of 0 for the number will always be right.Not much fun to play this game, if you ask me.Enable Session
The crux of enabling sessions in Web Services is in the EnableSession property of the WebMethod attribute.You need to explicitly set EnableSession to true in the WebMethods that need session support.In my example, this means all three method (but in theory, there may be methods that are unrelated to the game at hand, like an instruction method or an about box, that do not need to have the EnableSession property set to true).Note that if you use the EnableSession property (which can be seen in the following listing), you also have to derive the Web Service class from the base class System.Web.Services.WebService.This is needed because the derived Web Service class needs access to the Application and Session objects.
Once you've set EnableSession property to true, session management still doesn't really work, since the values for SessionNumber and SessionGuess fields are not maintained between method call (you can test the Game.asmx file in a browser to experience this).Although you have enabled sessions for your Web Service, you haven't actually used the storage "container" to store the values of SessionNumber and SessionGuess (and anything else you want to store and retrieve at a later point in the session).The ASP.NET storage container is of course the Session object, which can contain values that consists of a name and a value, with the following easy syntax:
Session["Name"] = Value;
You can store just about anything in the session, although you have to remember that the session will be kept in memory at the server.One unique Session object per unique client session (containing all name/value pairs for that session), so this may not be the best (scaleable) solution if hundreds of users decide to play this game at the same time.Note that there's also an Application object, but that one is global for all sessions, and obviously not a good idea to use here, since each player wants to guess his/her own unique number (based on the individual session).
Anyway, removing the declarations for the SessionNumber and SessionGuess fields, and making sure to store and retrieve the Number and Guess from the session using the aforementioned syntax, results in the new source code for the GuessWebServices, as seen below:
<%@ WebService Language="C#" Class="Game.GuessWebService" %>

using System;
using System.Web.Services;

namespace Game
{
[WebService(Namespace="http://www.eBob42.com")]
public class GuessWebService: System.Web.Services.WebService
{

[WebMethod(EnableSession=true)]
public void NewGame()
{
Session["Number"] = (new Random()).Next(0, 36);
Session["Guess"] = 0;
}

[WebMethod(EnableSession=true)]
public int Guess(int Number)
{
int SessionNumber = Convert.ToInt32(Session["Number"]);
Session["Guess"] = Convert.ToInt32(Session["Guess"]) + 1;
if (Number < SessionNumber) return -1;
else
if
(Number > SessionNumber) return 1;
else
{
return 0;
}
}

[WebMethod(EnableSession=true)]
public int Guesses()
{
return Convert.ToInt32(Session["Guess"]);
}
}
}
You can now deploy the Game.asmx in a scripts directory (for example as http://www.eBob42.com/cgi-bin/Game.asmx) and import it in order to use it in a web service client application.Importing Web Services
Using the WSDL command-line tool, you can import the Game web service and produce a GuessWebService.cs import unit with the GuessWebService proxy class.This import unit can be compiled to an assembly.You need to run the following two commands on the command-line to do this:
wsdl http://www.eBob42.com/cgi-bin/Game.asmx?WSDL
csc /t:library GuessWebService.cs
This will produce the GuessWebService.dll assembly, containing the GuessWebService proxy class that you can use in your web service client applications.An example console client application can be seen below:
using System;

namespace Game
{
class GuessGame
{
static void Main(string[] args)
{
GuessWebService MyGame = new GuessWebService();
MyGame.NewGame();
int Number = 0;
int GuessResult = 0;
do
{
Console.Write("New Guess (" + (MyGame.Guesses()+1).ToString() + "): ");
Number = Convert.ToInt32(Console.ReadLine());
GuessResult = MyGame.Guess(Number);
switch (GuessResult)
{
case 1: Console.WriteLine("Too High!"); break;
case -1: Console.WriteLine("Too Low!"); break;
case 0: Console.WriteLine("Well Done!"); break;
}
}
while (GuessResult != 0);
}
}
}
You need to compile this client application with the /r:GuessWebService.dll command-line option, to specify that you need to compile the source code and link the executable with a reference to the GuessWebService.dll assembly. However, after all is said and done, the client executable still doesn't work right! You can easily see this, since it will be asking for the first guess over and over again.Enable Sessions for the Client
Although you've enabled session support in the GuessWebService, that doesn't mean that any client will be able to work with sessions, too.The ASP.NET Web Services usually works with more than one client connection (each in its own unique session), so it must be able to know which incoming client request belongs to which Session object.The ASP.NET Web Service sends a cookie to the client with a unique session identifier (Session ID) that the client needs to use to identify itself when it wants to call subsequent methods - in the same session.The Session ID is unique for each session, and fortunately, the Session ID will also remain unchanged for the duration of the session.The cookies that are sent from the Web Service engine to the client are session cookies to be exact, so if you're using a browser they will not be stored on disk, but only kept in memory and returned automatically with new requests.Usually, however, the web service client is not a browser window, but a regular client executable, which doesn't automatically handle cookies.
To cut a long story short, in order to make sure the client executable returns the Session ID back to the Web Service, you need to receive and return the cookie with every request.Fortunately, this will be done automatically by the proxy class, after you've created the CookieContainer for this proxy class.In your example source code,.this means that you have to create a new CookieContainer and assign it to the CookieContainer property of the GuessWebService object (one additional line of code), as follows:
GuessWebService MyGame = new GuessWebService();
MyGame.CookieContainer = new System.Net.CookieContainer(); // Session Cookie
After you've made sure the CookieContainer is assigned, then the Web Service client will work as expected.To verify that each session is unique, run two or more instances of the client executable to "test" that they all use a different Session ID and hence talk to a different Session object on the server, with a different number to guess.Proxy Hack
Sometimes, there is a Web Service engine that I use in a number of my client applications.When this Web Service engine is statefull, it's a bit of a pain to remember to add the CookieContainer for every client application that I make.For those situations, I sometimes fall back to a little "hack" by editing the generated Web Service proxy class in the import file.In our example, that's the GuessWebService.cs file.Inside this file, you'll find the constructor of the proxy class, which typically consists of the following code for the constructor:
public GuessWebService() {
this.Url = "http://www.eBob42.com/cgi-bin/Game.asmx";
}
What you need to add, is a single line of code again, with a simple assignment to the CookieContainer in the constructor, again as follows:
public GuessWebService() {
this.Url = "http://www.eBob42.com/cgi-bin/Game.asmx";
this.CookieContainer = new System.Net.CookieContainer();
}
This will remove the need for the explicit assignment of the CookieContainer property in the client applications (you can still add a CookieContainer - or do it twice if you want - but it's no longer necessary, since there already is one).
Note that the GuessWebService.cs is an autogenerated file and any changes made to this file will be lost if the code is regenerated (but you can avoid that by not unnecessarily regenerating the import file, of course).It works fine for me, but please use at your own risk.Session Configuration
There are a number of session configuration settings you can consider when you really want to use session and state management in Web Services.The game that we've implemented is a nice statefull example, but when you exit a client game executable, the game may be over, but the session isn't.In fact, by default it will take another 20 minutes before the session - belonging to a game client that may not exist anymore - is released.During that time, the Session object takes up memory space (and also time, since more Session objects also means a slightly longer time to find the correct Session object based on the incoming Session ID).
In order to decrease the drain on the server, you can assign a new value - like 10 - to the Timeout property of the Session object, for example in the NewGame method.A value of 10 means 10 minutes.Every time the session is activated again (when the client calls another method with the EnableSession property set to true), the stopwatch resets.After 10 minutes of inactivity, the session is released.10 minutes may be good enough when the game starts, but when the user has guessed the right number, the Timeout can be set to something even lower.Like 1 minute (which gives the client application 1 minute to request the number of guesses - if needed - before the session is destroyed.If you try to be smart and set if to 0 minutes - or a negative number of minutes - you'll get an exception that explains that the argument to SetTimeout must be greater than 0.So, do you always have to wait at least one minute before a session is destroyed, you may ask? No, fortunately, you can also explicitly call the Abandon method of a Session, which will terminate it right away.You could call Abandon immediately after the right number has been guessed, but this has the sideeffect that the number of guesses (also stored in the Session object) will be gone as well.A better approach - not just for this example, but in general as well - is to use an explicit EndSession method (or in this case EndGame method) that will call the Session.Abandon method.
See the following code for the enhanced implementation of the GuessWebService using the Session.Timeout property and Session.Abandon method.
<%@ WebService Language="C#" Class="Game.GuessWebService" %>

using System;
using System.Web.Services;

namespace Game
{
[WebService(Namespace="http://www.eBob42.com")]
public class GuessWebService: System.Web.Services.WebService
{

[WebMethod(EnableSession=true)]
public void NewGame()
{
Session["Number"] = (new Random()).Next(0, 36);
Session["Guess"] = 0;
Session.Timeout = 10;
}

[WebMethod(EnableSession=true)]
public int Guess(int Number)
{
int SessionNumber = Convert.ToInt32(Session["Number"]);
Session["Guess"] = Convert.ToInt32(Session["Guess"]) + 1;
Session.Timeout = 1;
if (Number < SessionNumber) return -1;
else
if
(Number > SessionNumber) return 1;
else
{
return 0;
}
}

[WebMethod(EnableSession=true)]
public int Guesses()
{
return Convert.ToInt32(Session["Guess"]);
}

[WebMethod(EnableSession=true)]
public void EndGame()
{
Session.Abandon();
}
}
}
If the game client forgets to call the EndGame method, then it will still only be one minute before the session is automatically destroyed.This at least ensures that the Web Service doesn't use more server resources than necessary, while still enabling a lot of people to play this little game at the same time.Session Properties
Apart from the Session.Timeout property and Session.Abandon method, there are a few more helpful properties in the Session class (I won't cover them all, just the most useful in my view).The Count read-only property contains the number of items that are stored in the Session object (for the current session).Using the Keys property, you can retrieve all key values (the names) that are stored in the Session object - in case you don't know all names that have been stored in there.The IsNewSession read-only property is set to true if the session has just started with the current request (i.e.by calling this method), which can be helpful if you need to initialize the Session object.The SessionID read-only property contains the unique Session ID (this can be used as another proof that two different clients are using different sessions and are hence playing different instances of the game).
Finally, the special Mode property of the Session object is used to specify how the Session objects are managed by ASP.NET.When sessions are not used (EnableSession is set to false), then Mode has the value Off.If sessions are used, then the default value for Mode is InProc, meaning that the Session objects are maintained in memory on the web server machine.This is the best solution for statefull Web Services that run one a single web server machine.As a downside: if the ASP.NET worker needs to be terminated - even temporarily - then all Session objects and information will be terminated as well.
There are two alternative settings: SqlServer and StateServer.The latter means that the Session objects are maintained by the out-of-process running NT service state server, the aspnet_state.exe, which can even run on another machine (and will stay alive if the ASP.NET worker is terminated for some reason), and be shared by multiple Web Services on multiple machines, that can all point to the same state server on a special dedicated state machine.
The SqlServer setting means that the Session objects are actually stored in a SQL Server database, where session state data is placed into a blob field.This turns state management into a scaleable solution again (and is used for Web Services hosted in a Web farm for example), and is the only way where you can guarantee that session information is never lost.
Conclusion
Although Web Services are stateless by nature, it may sometimes be handy or necessary to add state management and session support to them.In this article, I've explained how they can be made statefull at the cost of being less scaleable.Apart from enabling sessions at the server side (in the Web Service engine), I've explained that you also need to enable cookies at the client or the proxy class in order to make sure the unique Session ID is received and returned for every call to the web service.
Finally, I've discussed some ways decrease the (negative) impact on your performance when using statefull Web Services.