Interface Inheritance

Managing State

HTTP is by nature a stateless protocol. Even with the introduction of the connection keep-alive protocol in HTTP 1.1, you cannot assume that all requests from a given client will be sent over a single connection. If the Web application needs to maintain state on behalf of the user, you often have to roll your own solutions.

Furthermore, state is usually scoped to the application. Application configuration parameters such as database connection strings are an example. Defining a Web application and providing a mechanism to store state that is scoped to the application is an implementation detail of the Web development platform.

The ASP development platform defines a Web application and provides a service for maintaining both session and application state. However, the ASP state management services have some serious limitations. ASP.NET provides a much-improved state management service. The service can be leveraged by Web Forms as well as Web services.

Session State

It is considered good practice to avoid having to maintain state between requests, when practical. For that reason, session state is disabled by default. You have to explicitly enable it for a particular Web method.

Maintaining state on behalf of a user involves associating multiple HTTP requests with one user session. ASP.NET uses a unique identifier that is passed by the client to identify the session. This identifier can be saved in a cookie maintained by the client or embedded within the URL of the request. Even though Web Forms supports both, Web services support only cookies.

If the proxy used by the client to access the Web service supports cookies, the session ID will automatically be sent with every request. ASP.NET uses a transient cookie to store the session ID. By definition, the cookie is intended to be maintained only for the life of the proxy used to access the Web service.

Because cookies are HTTP-specific, the session state mechanism is bound to the HTTP protocol. A transport protocol–agnostic way of passing the session ID would be to place the session ID within the header of the SOAP message. But this is not supported by ASP.NET, so you would have to roll your own state management system to support this scenario.

Once the session is identified, you need a repository to store the data associated with the session. The following three scenarios are supported, each with its advantages and disadvantages:

  • In Process This is the fastest scenario because calls to read/write session state will be handled in process. However, this is also the least robust configuration. If the ASP.NET worker process (aspnet_wp.exe) is terminated for any reason, all session state being maintained for the application will be lost. This configuration is ideal for Web services hosted on a single machine that need the most performant way of accessing state.

  • Out of Process In this configuration, session state is maintained in a separate process that can even reside on another machine. One advantage of this configuration is that if the ASP.NET worker process is terminated, the session state for the application will still be preserved. Because session state is maintained in memory, if the session state server (aspnet_state.exe) is terminated, all session state will be lost. Another advantage of this configuration is that state can be shared across multiple Web servers. All Web servers within the Web farm can be configured to point to the same state management process. This configuration is ideal for Web services hosted in a Web farm where the loss of state information should be avoided but is not critical.

  • SQL Server This is the most robust and scalable of the three configurations. Session state is maintained within a SQL Server database. The session state service maintains a set of tables in which the session state data is serialized into a binary blob. This is the ideal configuration for Web services hosted in a Web farm if you can afford to purchase and maintain SQL Server. This configuration is mandatory if you need to ensure that session state is never lost.

Of the three configurations, In Process is the only one available via the .NET Framework. You must purchase either the Professional or Enterprise Edition of ASP.NET to obtain the Out of Process and SQL Server configuration options.

To use the ASP.NET session state service, you must add the module named SessionStateModule to the application. The default machine-wide configuration file (C:\WINNT\Microsoft.NET\Framework\version\CONFIG\machine.config) adds this module.

Once you add SessionStateModule, you can configure the session state service within the sessionState element of the machine.config or web.config configuration file. Table 6-6 lists the attributes that you can set within the sessionState element.

Table 6-6  Attributes of the sessionState Element

Attribute

Description

mode

Specifies where ASP.NET will save session state. The possible values are

Off Session state is disabled.

InProc Session state is stored within the ASP.NET worker process.

StateServer Session state is stored by the out-of-process session state server.

SqlServer Session state is stored within SQL Server.

The default is InProc.

cookieless

Specifies whether cookieless sessions should be enabled. The default is false.

timeout

Specifies the number of minutes the session can be idle before the session is abandoned. The default is 20 minutes.

stateConnectionString

Specifies the location of the session state server. The default value is tcpip=127.0.0.1:42424.

sqlConnectionString

Specifies the location of the SQL server. The default value is data source=127.0.0.1;user id=sa;password=.

Once you have the session state service properly configured, session state is enabled on a per-Web-method basis. You can enable session state for a particular Web method by setting the EnableSession property of the WebMethod attribute to true.

Regardless of which configuration you choose, the API for reading/writing session state is exactly the same. The class that contains the Web method should inherit from the WebService class. The WebService class exposes the Session property, which returns an instance of the HttpSessionState class, otherwise known as the session object.

The session object is used to maintain a collection of information related to the user's session. Items can be added to and retrieved from the collection via an int or string indexer.

The following example expands the Securities Web service to use session state. The SetCurrency Web method allows the client to select a particular currency. Future calls to InstantQuote will return the price of the security using the selected currency.

using System; using System.Web.Services; namespace BrokerageFirm {     [SoapRpcService]     public class Securities : WebService     {         public Securities()         {             // Set the default value of the target currency.             if(this.Session["TargetCurrency"] == null)             {                 this.Session["TargetCurrency"] = CurrencyType.US_DOLLAR;             }         }         public enum CurrencyType         {             US_DOLLAR,             UK_POUND,             GE_DEUTSCHMARK         }         [WebMethod(true)]         public void SetCurrency(CurrencyType targetCurrency)         {             this.Session["TargetCurrency"] = targetCurrency;         }         [WebMethod(true)]         public double InstantQuote(string symbol)         {             // Implementation...             return Convert(price,              (CurrencyType)this.Session["TargetCurrency"]);         }         private double Convert(double usPrice, CurrencyType targetCurrency)         {             double targetCurrencyPrice = usPrice;             // Implementation ...             return targetCurrencyPrice;         }     } }

The SetCurrency method persists the client's currency preference within the session. The InstantQuote method then retrieves the currency preference from the client's session and converts the price of the security appropriately.

As shown in the preceding example, you can use the string indexer to both set and retrieve values from the session object. However, you can use the int indexer only to retrieve values contained within the session object. You can also use the Add method to add items to the collection managed by the session object.

Because the client might not have selected a target currency, a default value is set within the Securities object constructor. Even with session state enabled, ASP.NET will still create a new instance of the Securities object for every request. The constructor will initialize the value of the target currency only if the value is null.

A potential issue can arise in the preceding example if the client does not support cookies. By default, ASP.NET clients do not support cookies. In the example, a client that does not support cookies will always have the price of a stock returned in U.S. dollars. A better design would be to extend the method signature of InstantQuote to accept the symbol of the security as well as the targeted currency. This would also eliminate a network round-trip because the client would no longer need to call the SetCurrency Web method.

The session object also supports the ICollection and IEnumerable interfaces, which allow polymorphic enumeration through the items within the collection. The following example uses the IEnumerable interface to iterate through the collection:

[WebMethod(true)] public override string ToString() {     StringBuilder sb = new StringBuilder();     foreach(string index in this.Session)     {         sb.AppendFormat("{0} = {1}\n", index, this.Session[index].ToString());     }     return sb.ToString(); }

This method declaration overrides the Object.ToString method and exposes it as a Web method. The implementation of the Web method enumerates through the session object via the IEnumerable interface by using the foreach keyword. Each name/value pair stored within the session object is appended to an instance of the StringBuilder class. Finally the resulting string is returned from the Web method.

Application State

State that is global to the application can be stored within the application object. An example of this is a database connection string. Unlike session state, application state is always handled in process and cannot be shared between servers in a Web farm. Also unlike session state, application state is not dependent on the client supporting cookies.

Classes that derive from the WebService class expose the Application property. This property retrieves an instance of the HttpApplicationState object containing state that is global to the Web application. The HttpApplicationState class derives from the NameObjectCollectionBase class. Because the implementation of the NameObjectCollectionBase class creates a hash table, retrieving a particular value from the application object is very efficient.

Let's say I want to implement a counter to record the number of times the Web service has been accessed because the application has been started. I could add the following code to the InstantQuote method just before I return the price to the customer:

// Record the access to the Web service. this.Application["HitCounter"] = (int)this.Application["HitCounter"] + 1;

Unfortunately, the code has two problems. First, the HitCounter application variable is never initialized. Every time the above code is executed, it will generate an exception. Second, because multiple clients can potentially increment the HitCounter application variable simultaneously, a potential race condition might occur. Let's address these issues one at a time.

ASP.NET provides a framework for handling application startup code within a Web service. Every Web application can contain a global.asax file. Within the file, you can implement code that is executed when certain predefined events occur, such as application startup/shutdown and session start up/shutdown. Application startup is an ideal point to initialize application variables. The following code initializes the HitCounter application variable during the application start event within the Global.asax page:

using System; using System.Web; namespace BrokerageFirm  {     public class Global : HttpApplication     {         protected void Application_Start(Object sender, EventArgs e)         {             // Initialize the hit counter to 0.             this.Application["HitCounter"] = (int)0;         }     } }

In the Application_Start method, I initialize the HitCounter application variable to zero. I also explicitly cast it to an int to avoid any ambiguity. Because the ASP.NET runtime executes the Application_Start method once during the life of the application, you do not have to worry about concurrency issues.

However, the InstantQuote method can be called by multiple clients simultaneously. Therefore, you must avoid potential race conditions when you update the data. Even though incrementing the HitCounter application variable is represented by a single line of C# code, the single line will be translated into multiple machine instructions when it is compiled. Here is the resulting IL code:

  IL_0074:  ldarg.0   IL_0075:  call       instance class                         [System.Web]System.Web.HttpApplicationState                         [System.Web.Services]System.                        Web.Services.WebService::get_Application()   IL_007a:  ldstr      "HitCounter"   IL_007f:  ldarg.0   IL_0080:  call       instance class                         [System.Web]System.Web.HttpApplicationState                         [System.Web.Services]System.Web.Services.                        WebService::get_Application()   IL_0085:  ldstr      "HitCounter"   IL_008a:  callvirt   instance object                         [System.Web]System.Web.HttpApplicationState::                        get_Item(string)   IL_008f:  unbox      [mscorlib]System.Int32   IL_0094:  ldind.i4   IL_0095:  ldc.i4.1   IL_0096:  add   IL_0097:  box        [mscorlib]System.Int32   IL_009c:  callvirt   instance void                         [System.Web]System.Web.HttpApplicationState::                        set_Item(string,object)   IL_00a1:  ldarg.0   IL_00a2:  call       instance class                         [System.Web]System.Web.HttpApplicationState                         [System.Web.Services]System.Web.Services.                        WebService::get_Application()

The single line of C# code translates to 15 lines of IL, and then the IL is compiled to numerous machine codes before it is executed. Because the code can be executed simultaneously by two or more clients, this will lead to unpredictable results.

As an example of the problems that can occur if two clients (A and B) attempt to run the same code simultaneously, suppose that the HitCounter application variable was initially set to 1 and client A executes the above IL to increment the value to 2. IL_008a obtains the initial value of 1 for HitCounter. IL_009c sets HitCounter to a new value of 2. Suppose also that client B updates the value of HitCounter to 2 somewhere between IL_008a and IL_009c. Because client A will be incrementing the previously retrieved value of 1, HitCounter will be incorrectly set to 2 instead of the correct value of 3.

The application object provides a locking mechanism to ensure that writes made to the data are performed serially, thereby avoiding race conditions such as the one described in the preceding paragraph. Operations that require serialized access to the data can be performed between the Lock and Unlock methods provided by the application object. The following example properly updates the HitCounter application variable each time the InstantQuote Web method is invoked:

using System; using System.Web.Services; namespace BrokerageFirm {     [SoapRpcService]     public class Securities : WebService     {         public enum CurrencyType         {             US_DOLLARS,             UK_POUNDS,             GE_DEUTSCHMARKS         }         [WebMethod]         public double InstantQuote(string symbol,          CurrencyType targetCurrency)         {             double price = 0;             // Implementation...             // Record the access to the Web service.             this.Application.Lock();             this.Application["HitCounter"] =              (int)this.Application["HitCounter"] + 1;             this.Application["LastSymbol"] = symbol;             return Convert(price, targetCurrency);         }         private double Convert(double usPrice,          CurrencyType targetCurrency)         {             double targetCurrencyPrice = usPrice;             // Implementation...             return targetCurrencyPrice;         }     } }

By locking the application object before attempting to increment it, you ensure that you will have exclusive access to the lock on the application object. Because all calls to Unlock will be blocked, you should call the Unlock method as quickly as possible to avoid hindering throughput. Note, however, that even when you have locked the application object, you do not have exclusive access to the data. Therefore, to avoid race conditions from being introduced into your application, you must ensure that a common locking scheme is used throughout your application.

You should also look for opportunities where application scoped data can be updated without locking the application object. Notice that I also updated the LastSymbol application variable with the last symbol that was successfully processed by the Web method. In this case, I was not concerned about race conditions because by definition the last security quoted would have been processed by the Web method that last updated the LastSymbol application variable.

If both the LastSymbol and the LastPrice application variables needed to be set, I would have updated both of them before unlocking the application object. This would avoid a situation in which client A was the last one to update LastPrice and client B was the last one to update LastSymbol.

Before moving to the next topic, I want to offer a word of caution about the use of the Lock and Unlock methods. You should ensure that every time a Web method calls Lock, Unlock is called as soon as possible; otherwise, you run the risk of blocking other requests that are currently being processed by the Web service. A good design pattern is to call the Unlock method within the finally section of a try/catch block. Here is an updated example of the Purchase method:

// Record the access to the Web service. try {     this.Application.Lock();     this.Application["HitCounter"] = (int)this.Application["HitCounter"] + 1; } catch(Exception e) {     // Handle exception ... } finally {     this.Application.UnLock(); } // Significant processing to quote the price... this.Application["LastSymbol"] = symbol;

Because the Unlock method call was placed within the finally section of the try/catch block, Unlock will be called even if the code to update the HitCounter application variable fails (for example, when an OverflowException is thrown as a result of the addition). This ensures that other ASP.NET worker threads will not be needlessly blocking on a call to Lock.

What if you forget to unlock the application object before your Web method returns? A mistake such as this could have a detrimental effect on your application. The next time you try to obtain the lock for the application object, the call to Unlock will deadlock. Fortunately, the ASP.NET runtime prevents this from happening. When a Web method returns, the ASP.NET runtime ensures that the lock obtained on the application object is freed.

One of the biggest problems with using the application object to implement a hit counter is that it is the developer's responsibility to ensure that the application object is locked before the counter is incremented. A better alternative would be to leverage static properties. As with the application object, static properties are scoped to the application. Unlike the application object, you can associate behavior with static properties. For example, consider the following HitCounter class.

public class HitCounter {     private static int count = 0;     private static object countLock = new object();     private HitCounter() {}     public static int Count     {         get { return count; }     }     public static void Increment()     {         lock(countLock)         {             count++;         }     } }

Instead of storing the hit counter within the application object, I define a class that implements a property and a method for accessing and manipulating the hit counter. Because the field containing the current count is declared as private, developers that use the class cannot increment it directly. Instead, the HitCounter class exposes a public read-only static property to access the current count and a public static method to increment the hit counter. The Increment method uses the lock keyword to ensure that there is no potential for a race condition while incrementing the counter.



Building XML Web Services for the Microsoft  .NET Platform
Building XML Web Services for the Microsoft .NET Platform
ISBN: 0735614067
EAN: 2147483647
Year: 2002
Pages: 94
Authors: Scott Short

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net