IIS/ASP.NET Cookieless Support Not Working As Expected

In one of the environments I work, cookies cannot be used because the pages run inside web browser controls running on a client application and cookies end up being shared by all browsers.

Fortunately, ASP.NET allows us to persist some cookies as part of the URL.

To persist the session state identifier cookie in the URL we just need to add the following configuration:

<configuration>
  <system.web>
    <sessionState cookieless="UseUri" />
  </system.web>
</configuration>



and you’ll get URLs like this:



http://localhost/Cookieless/(S(jcmwek3ja0lvdpbwoacpjirv))/default.aspx




The way IIS and ASP.NET do this is by IIS removing the section between parenthesis after the virtual directory path and adding the AspFilterSessionId HTTP header to the request. Than, ASP.NET picks it up and extracts the cookie.




I wrote this simple page to demonstrate this working:



<%@ Page Language="C#" AutoEventWireup="true" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <table>
            <tr>
                <td>Raw URL</td>
                <td><%= Request.RawUrl %></td>
            </tr>
            <tr>
                <td>Cookiless Cookies<br />AspFilterSessionId Request HTTP Header</td>
                <td><%= Request.Headers["AspFilterSessionId"] %></td>
            </tr>
            <tr>
                <td>Session ID</td>
                <td><%= Session.SessionID %></td>
            </tr>
        </table>
    </div>
    </form>
</body>
</html>



For the above URL, we’ll get a page like this:




Raw URL /Cookieless/default.aspx
Cookiless Cookies
AspFilterSessionId Request HTTP Header
S(jcmwek3ja0lvdpbwoacpjirv)
Session ID jcmwek3ja0lvdpbwoacpjirv




IIS strips these cookies even for serving static content like cascading stylesheets.




You can test this by creating a default theme. You can do this by adding a Default folder under the App_Themes folder and adding a Styles.css file to it:



body
{
    background-color: Yellow;
}
table, tr, td
{
    border: thin solid black;
}



and setting the theme as default using the following configuration:



<configuration> <system.web> <sessionState cookieless="UseUri" /> <pages theme="Default"/> </system.web> </configuration>




And you’ll get a "pretier" page:




Raw URL /Cookieless/default.aspx
Cookiless Cookies
AspFilterSessionId Request HTTP Header
S(jcmwek3ja0lvdpbwoacpjirv)
Session ID jcmwek3ja0lvdpbwoacpjirv




If you have special needs for your session state identifiers, you can implement your own session identifier manager.




But if you want to use cookieless cookies, you only have one way to do it: extend the SessionIDManager class:



public class SessionIdManager : System.Web.SessionState.SessionIDManager
{
    public override string CreateSessionID(System.Web.HttpContext context)
    {
        string id = System.Guid.NewGuid().ToString("B");

        return id;
    }

    public override bool Validate(string id)
    {
        try
        {
            new System.Guid(id);
            return true;
        }
        catch
        {
            return false;
        }
    }
}



and configure the session state module to use it:



<configuration>
  <system.web>
    <sessionState cookieless="UseUri" sessionIDManagerType="SessionIdManager" />
    <pages theme="Default"/>
  </system.web>
</configuration>



And we’ll end up with this nice page:



http://localhost/Cookieless/(S(%7b0861e55a-e29b-4b6f-825b-1e1d4c57f095%7d))/default.aspx




Raw URL /Cookieless/(S({0861e55a-e29b-4b6f-825b-1e1d4c57f095}))/default.aspx
Cookiless Cookies
AspFilterSessionId Request HTTP Header
 
Session ID {0861e55a-e29b-4b6f-825b-1e1d4c57f095}




OOPS! What happened here?




Looks like IIS was unable to transfer the cookies to the appropriate HTTP header but ASP.NET was able to find the requested resource. On the other hand, IIS couldn’t find the http://localhost/Cookieless/(S(%7b0861e55a-e29b-4b6f-825b-1e1d4c57f095%7d))App_Themes/Default/Styles.css.




This happens in these environments:



Operating System IIS ASP.NET
Windows XP Pro SP3 5.1 2.0 SP1, 3.5
Windows Server 2003 R2 6 2.0 SP1, 3.5
Windows Server 2008 7 2.0 SP1, 3.5





Fortunately, in IIS 7 you can have HTTP modules in integrated pipeline mode that are called for every resource requested to IIS.




Your module doesn’t even need to do nothing. It just needs to exist:



public class Module : System.Web.IHttpModule
{
    public void Dispose()
    {
    }

    public void Init(System.Web.HttpApplication context)
    {
    }
}



and be added to the configuration:



<configuration>
  <system.web>
    <sessionState cookieless="UseUri" sessionIDManagerType="SessionIdManager" />
    <pages theme="Default"/>
  </system.web>
  <system.webServer>
    <modules>
      <add name="Module" preCondition="integratedMode" type="Module" />
    </modules>
  </system.webServer>
</configuration>



And our “pretty” page is back:



Raw URL /Cookieless/default.aspx
Cookiless Cookies
AspFilterSessionId Request HTTP Header
S({0861e55a-e29b-4b6f-825b-1e1d4c57f095})
Session ID {0861e55a-e29b-4b6f-825b-1e1d4c57f095}




Is it just me, or there’s something definitely wrong here?




That’s why I opened this bug on Microsoft Connect

Other Ways For Making PathInfo And ASP.NET Themes Work Together

On my last post I wrote about a solution for the problem that arises when we try the use path infos and ASP.NET Themes and Skins together.

Raj Kaimal suggested rewriting all LINK HTML elements URLs to the correct URL as seen from the client. Something like this:

void HttpApplicationPreRequestHandlerExecute(object sender, System.EventArgs e)
{
    var httpApplication = sender as System.Web.HttpApplication;

    var httpContext = httpApplication.Context;

    var page = httpContext.CurrentHandler as System.Web.UI.Page;

    if ((page != null) && !string.IsNullOrEmpty(httpContext.Request.PathInfo))
    {
        page.PreRenderComplete += delegate(object source, System.EventArgs args)
        {
            var p = source as System.Web.UI.Page;

            foreach (System.Web.UI.Control headerControl in p.Header.Controls)
            {
                var link = headerControl as System.Web.UI.HtmlControls.HtmlLink;
                if (link != null)
                {
                    link.Href = p.ResolveUrl(link.Href);
                }
            }
        };
    }
}



With this approach you still have a problem (which mine didn’t solve) with post backs because the rendering of the ACTION of the HTML FORM is also broken.



Israel Aéce suggested the use of the BASE HTML element to re-base relative URLs. Something like this:



void HttpApplicationPreRequestHandlerExecute(object sender, System.EventArgs e)
{
    var httpApplication = sender as System.Web.HttpApplication;

    var httpContext = httpApplication.Context;

    var page = httpContext.CurrentHandler as System.Web.UI.Page;

    if ((page != null) && !string.IsNullOrEmpty(httpContext.Request.PathInfo))
    {
        page.PreRenderComplete += delegate(object source, System.EventArgs args)
        {
            page.Init += delegate(object source, System.EventArgs args)
            {
                var p = source as System.Web.UI.Page;

                var htmlBase = new System.Web.UI.WebControls.WebControl(System.Web.UI.HtmlTextWriterTag.Base);
                htmlBase.Attributes.Add("href", p.Request.Url.GetLeftPart(System.UriPartial.Authority) + p.Request.CurrentExecutionFilePath);
                p.Header.Controls.Add(htmlBase);
            };
        };
    }
}



This seems like the better solution except if your site sits behind several security perimeters and it is not possible to be sure what the domain is as seem from the client side, which was my problem to begin with.



But if you are thinking of calling Server.Execute, Server.TransferRequest or Server.TransferRequest, neither of these two solutions will work.

Making PathInfo And ASP.NET Themes Work Together


Updated on 2008.07.28 – The code was done in a hurry and, talking to my friend Luís, I noticed that I had forgotten to make a case insensitive comparison and that the code was not so obvious. So, I updated the code and added an explanation.


On my last post I wrote about the problem that arises when we try the use path infos and ASP.NET Themes and Skins together.


But most of the times you don’t care about the why you can’t. You just want to know how you can.


The way I see it, the right solution would be to render the URLs for the stylesheets rooted.


But since I can’t do that, the next best thing is the serve the wrongly addressed request properly.


But how can we do that?


The only way I could come up with, was an HTTP Module:

public class AppThemesModule : global::System.Web.IHttpModule
{
    private const string LocalThemesFolderName = “/App_Themes/”;
    private static readonly int searchStartIndex;
    private static readonly int minimumLenghtForSearch;

    static AppThemesModule()
    {
        int searchStartIndex = System.Web.HttpRuntime.AppDomainAppVirtualPath.Length;

        AppThemesModule.searchStartIndex = ((searchStartIndex == 1) ? 0 : searchStartIndex) + 2;

        AppThemesModule.minimumLenghtForSearch = AppThemesModule.searchStartIndex + AppThemesModule.LocalThemesFolderName.Length;
    }

    #region IHttpModule Members

    public void Dispose()
    {
    }

    public void Init(System.Web.HttpApplication context)
    {
        context.BeginRequest += HttpApplicationBeginRequest;
    }

    #endregion

    void HttpApplicationBeginRequest(object sender, System.EventArgs e)
    {
        System.Web.HttpApplication httpApplication = sender as System.Web.HttpApplication;

        string path = httpApplication.Request.Path;
        if (path.Length > searchStartIndex)
        {
            int appThemesStartIndex = path.IndexOf(AppThemesModule.LocalThemesFolderName, searchStartIndex, System.StringComparison.OrdinalIgnoreCase);
            if (appThemesStartIndex > 0)
            {
                httpApplication.Context.RewritePath(“~” + path.Substring(appThemesStartIndex));
            }
        }
    }
}

The code starts by initializing the static read-only field searchStartIndex with the start index of the search for the /App_Themes/ pattern. If the length of the application’s virtual path is 1, that means that it’s the root of the site and search start index is 0 instead of 1; otherwise the search start index will be the length of the application’s virtual path. 2 is added because there is no need to start searching the path just after the application’s virtual path (if the pattern was found just after the application’s virtual path, no replacement would be needed).


Than, the static read-only field minimumLenghtForSearch is initialized with the minimum length of the path to search for the pattern. There is no need to search for the pattern on paths shorter than the search start index plus the length of the pattern because, if found, no replacement would be needed.


Besides registering the module, you’ll have to configure your virtual directory so that all the files to be served out of the themes are handled by a StaticFileHandler.

PathInfo And ASP.NET Themes: Why Can’t They Work Together?

If you ever worked with ASP.NET Themes and Skins, you know that stylesheet links are added to the head section of the HTML document.

The rendered URL to these stylesheets is always relative to location of the page being requested.

So, for a request to:

http://MySite/Section/Default.aspx

you’ll get:

<link href="../App_Themes/Default/Styles.css" type="text/css" rel="stylesheet" />

which will make the web browser request for:

http://MySite/App_Themes/Default/Styles.css

and it all works fine.

Well, it works fine until you need to navigate to:

http://MySite/Section/Default.aspx/PathInfo

You’ll get the same stylesheet reference and the browser will request for:

http://MySite/Section/Default.aspx/App_Themes/Default/Styles.css

This happens because the web browser has no knowledge of what PathInfos are. It only accounts for the number of forward slashes (/).

I’ve filed a bug on Microsoft Connect about this.

Make The HttpValueCollection Class Public And Move It To System.DLL

I find the System.Web.HttpValueCollection class very useful in a wide number of situations that involve composing HTTP requests or any other need to represent name/value collection as a string (in an XML attribute, for example).

As of now (.NET Framework 3.5 SP1 Beta), the only way to create an instance of the System.Web.HttpValueCollection class is using the System.Web.HttpUtility.ParseQueryString method.

I’d like to see it public and available in a more generic assembly like System.DLL to be available on every type of .NET application (Windows Client applications, Windows Service applications, Silverlight applications, Windows Mobile applications, etc.).

If you agree with me, vote on my suggestion on Microsoft Connect.

BEWARE: System.Web.HttpValueCollection Parsing Is Not Reversible

If you run this code:

System.Collections.Specialized.NameValueCollection queryString = System.Web.HttpUtility.ParseQueryString("noKey&=emptyKey&A=Akey");



queryString will actually have the running type of System.Web.HttpValueCollection.




What’s great about this class is that its ToString method output is the collection’s content in a nice URL encoded format.




As with its base class (NameValueCollection), there’s a difference between a null string and an empty string key and the parsing treats query string parameters with no parameter specification as having a null string key and parameters with an empty string key having an empty string parameter key.




So, when call ToString on the instance returned by System.Web.HttpUtility.ParseQueryString method you would expect to get the parsed string (or, at least, one that would be parsed into the equivalent collection), right? But what you’ll get instead is this: noKey&emptyKey&A=Akey.




I’ve filed a bug into connect. If you think this is important and must be corrected, please vote.

Isolator 4.3 Released!

Today Typemock released version 4.3 of Typemock Isolator. Download it from here.

What’s new?

  • Support for Ivonna. For those of you who develop ASP.Net applications, Ivonna is a great tool, built on top of Isolator’s platform, to simplify writing tests for ASP.NET.
  • Typemock.Integration.Packs namespace APIs added to support license management through Isolator, the way Ivonna does.
  • As announced when it was released, version 4.2 was the last version of Isolator to support .NET 1.1. Version 4.3 only supports the 2.0 runtime and its Visual Studio counterparts: VS2005 and VS2008.
  • For 64 bit machines, now there’s a single installer. (don’t forget to uninstall both previous 32 and 64 installers prior to installing 4.3.)
  • RecorderManager.GetMockOf(instanceRef) and MockManager.GetMockOf(instanceRef). To retrieve the mock controller object based on a reference to the instance. (more…).

Bug fixes:

  • Fixes to DLINQ support. LINQ Queries with data tables now work better, for example with GroupBy.
  • Static constructors in Natural Mocks are now invoked correctly.
  • A bug that caused an exception to be thrown when mocking interfaces ("Method XX in type IMyInterface has no matching overload that returns TypeMock.Mock+a) was fixed.
  • A bug that caused an exception to be thrown when the mocked object was overriding Equals was fixed.
  • A bug that caused failure in mocking explicit interface with the same name was fixed.
  • A bug occurring im multithreading scenarios in VerifyWithWait was fixed.
  • A bug that causes NullReferenceException to be thrown when using Auto Deploy was fixed.

See also: