Transferring Data Between ASP.NET Web Pages

There are a huge number of articles and blog posts on persisting data between user requests in ASP.NET. However, the ASP.NET Web Pages framework introduces a couple of additional mechanisms so this article explores those, as well as reviewing some of the standard approaches already available to Web Pages developers from other parts of the ASP.NET framework.

I'll begin by clarifying the problem that needs to be solved: HTTP is a stateless protocol. Being stateless, there is no requirement placed on HTTP (Web) Servers to retain information about each request or user, so by default, multiple requests from the same user are treated as a series of individual unconnected requests. In fact, the server has no concept of a "user" as such. If you want to manage user or application-related data over a number of requests, you have to implement strategies for managing that yourself. This article explores a number of client and server side options that ASP.NET offers to help facilitate this for the Web Pages developer:

  • Hidden Form Fields
  • Query Strings
  • UrlData
  • Cookies
  • Session Variables
  • Application Variables
  • Cache

Each of the mechanisms provided by the ASP.NET Web Pages framework for managing this problem has its pros and cons as you will see.

Hidden Form Fields

You use the <input type="hidden" /> HTML element to add a hidden field to a form, or you can use the Html.Hidden() helper. They are not visible to the user so the user is not able to enter values directly into the hidden field. However, as a developer you can set their value from both client-side and server-side code. For an example of the value of hidden fields being set from client-side code, have a look at this previous article on using jCrop. In the meantime, here's an example that illustrates the use of hidden fields in a quiz application. The application consists of a multi-page form, which is a prime candidate for hidden fields as users' selections need to be remembered from one page to the next. The first page (Page1.cshtml) contains the first question in a form:

<form method="post" action="/Page2">
    <div>Q1: Where is the Olympics 2012 being held?</div>
    <input type="radio" name="Q1" value="London" /> London<br />
    <input type="radio" name="Q1" value="Paris" /> Paris<br />
    <input type="radio" name="Q1" value="Moscow" /> Moscow<br />
    <input type="submit" value="Question 2..." />
</form>

Notice that the action attribute of the form points to Page2.cshtml. Here's the code for that:

<form method="post" action="/Page3">
    <div>Q2: Where is the World Cup 2014 being held?</div>
    <input type="radio" name="Q2" value="Canada" /> Canada<br />
    <input type="radio" name="Q2" value="Brazil" /> Brazil<br />
    <input type="radio" name="Q2" value="Lagos" /> Lagos<br />
    <input type="submit" value="Question 3..." />
    @Html.Hidden("Q1", Request["Q1"])
</form>

The hidden field (generated here from the Html.Hidden helper) is populated with the value from the user's selection in the previous page. When this form is submitted to Page3.cshtml, the value of the hidden field will again be available as Request["Q1"], where it, along with the value from question 2 are stored again in hidden fields:

<form method="post" action="/Finish">
    <div>Q3: Where is the Tour de France held?</div>
    <input type="radio" name="Q3" value="Germany" /> Germany<br />
    <input type="radio" name="Q3" value="Serbia" /> Serbia<br />
    <input type="radio" name="Q3" value="France" /> France<br />
    <input type="submit" value="Finish!" />
    @Html.Hidden("Q1", Request["Q1"])
    @Html.Hidden("Q2", Request["Q2"])
</form>

On the final page, all values are processed:

@{
    Page.Title = "Hidden Fields"; 
    var numberCorrect = 0;
    if (!Request["Q1"].IsEmpty() && Request["Q1"].Equals("London")){
        numberCorrect+= 1;
    }
    if(!Request["Q2"].IsEmpty() && Request["Q2"].Equals("Brazil")){
        numberCorrect+= 1;
    }
    if(!Request["Q3"].IsEmpty() && Request["Q3"].Equals("France")){
        numberCorrect+= 1;
    }    
}
<h1>Hidden Fields</h1>
<div>You scored @numberCorrect out of 3!</div>
<p>Q1: You said "@Request["Q1"]". Correct answer is London<br />
    Q2: You said "@Request["Q2"]". Correct answer is Brazil<br />
    Q3: You said "@Request["Q3"]". Correct answer is France</p>

While a hidden field is not visible to the user, that does not make it secure. Values are stored and transmitted in plain text (unless you choose to incorporate your own encryption method) and are accessible in the HTTP request as well as the HTML source that your browser makes available to you. You should not use hidden fields to store sensitive data - although I have seen examples of people using hidden fields to store database connection strings and even partial SQL to be executed against databases. Mental. Equally, hidden field values are not tamper-proof either, and should really be validated just like any other user input. As with any form field, if the POST method is used, there is no limit to the number of characters that can be stored in a hidden field.

Query Strings

A query string is a collection of name/value pairs which are appended to a URL. They are separated from the location of the resource by a question mark ?. An example might look like this:

http://www.mydomain.com?name1=value1&name2=value2&name3=value3

Each name/value pair is separated from the others by the & sign. The values are obtained from the Request.QueryString collection. For example, Request.QueryString["name1"] will yield value1 as will the shorter version: Request["name1"]. Often, hyperlinks with query strings are generated dynamically from code. You might list items from a database for example, and link to a page that contains their details:

@foreach (var row in data) {
    <link /><a href="/Details.cshtml?id=@row.ItemId">Details</a>
}

In Details.cshtml, you would use Request["id"] to obtain the value of the item to display from the query string. Another way to generate query strings is to use the GET method with forms:

<form action="/QueryStringValues">
    <div>Provide values for each query string token:</div>
    <div>a: <input type="text" name="a" /></div>
    <div>b: <input type="text" name="b" /></div>
    <div>c: <input type="text" name="c" /></div>
    <div><input type="submit" /></div>
</form>

Notice that the method attribute is not specified in the form tag. As a result, HTTP GET will be used because it is the default method. If you were to enter "1", "2", and "3" into the a, b and c inputs ans submitted this form, the resulting URL will look like this (port number will vary):

 http://localhost:4954/QueryStringValues?a=1&b=2&c=3 

Like hidden fields, query strings should not be used for sensitive data and it is even easier for a user to manipulate query string values so input validation is really important. Just because you generated the query string from your code, that doesn't mean that a malicious user can't generate their own or alter yours in their browser address bar. Unvalidated query string values are the primary route to attacks on web sites. Most browsers limit query strings to around 2000 characters so they are not suitable for managing large amounts of data. However, search engines can follow links with query string values and index their location.

UrlData

UrlData is unique to the Web Pages framework. Similar to query strings, UrlData forms part of a URL. It appears as a series of arbitrary values that follow on from the file name of the page being requested. In the example below, the page being requested is UrlData.cshtml, and three values - value1, value2 and value3 are appended to the URL:

http://www.mydomain.com/UrlData/value1/value2/value3

UrlData values are treated as a collection - a List<string> in fact, and values are referenced by their index within the collection. For example, from within UrlData.cshtml, UrlData[0] will yield value1. URLs with UrlData can be generated dynamically just like query strings, and the same rules apply regarding size, precautions against tampering, visibility as with query strings. The main difference between UrlData and query strings is that the former can be used to generate much more SEO-friendly URLs compared to query strings.

In the sample download that accompanies this article, a hyperlink containing UrlData is generated dynamically:

<h1>UrlData</h1>
<a href="/ReadUrlData/@DateTime.Now.Day/@DateTime.Now.Month/@DateTime.Now.Minute">Click to read UrlData</a>

The code in ReadUrlData.cshtml obtains the values:

@for (var i = 0; i < UrlData.Count(); i++) {
    <div>UrlData[@i] is @UrlData[i]</div>
}

At the moment, the generated URL is http://localhost:4954/ReadUrlData/13/7/43 (7:43am on July 13th), so the code outputs the following:

UrlData[0] is 13
UrlData[1] is 7
UrlData[2] is 43

 

Cookies

Cookies are small pieces of text that are passed between browser and web server with every request. As a consequence, their values are available to any page within the site. Cookies are commonly used to store user preferences which help the site remember which features to turn on or off, for example. They might be used to record the fact that the current user has authenticated and is allowed to access restricted areas of the site. It is the browser's job to store persistent cookies as text files on the client machine. The storage duration is determined by the type of cookie and the expiry date that it is given. Cookies with no expiry date are not stored on the client machine and are cleared at the end of the user's session.

You create or set a cookie like this:

Response.Cookies["myCookie"].Value = "some cookie value";

That's it. All the time that the user's session continues, you can read the value of the cookie:

var cookieValue = Request.Cookies["myCookie"].Value;

This is a single value cookie. You can add mutliple values to a cookie by using subkeys:

Response.Cookies["myCookie"]["user"] = "mike";
Response.Cookies["myCookie"]["role"] = "admin";

And to persist a cookie, you set an expiration time. Here, the cookie is set to persist for 6 months from today:

Response.Cookies["myCookie"].Expires = DateTime.Now.AddMonths(6);

Cookies are limited to 4Kb in size and transferred in plain text, so they are not the place for sensitive or large volumes of data. They can be deleted by users randomly so you ashould always check to make sure they are not null before trying to access a value from one otherwise you might end up with NullReferenceExceptions at runtime.

Session Variables

Session State provides a mechanism that enables you to tie together requests from the same user for a limited period - the duration of a session. As such, you can store user-related values and retrieve them at any stage during a session. By default, a session begins when the user first visits the site, and lasts for 20 minutes after the last request. Session variables can be any data type from a string to a collection. When the item is stored in session, it is stored as an object data type, so it will need to be cast back to its original data type if there is no implicit cast between object and the original type. The following code shows how to set two session variables - one to hold a string and the other to store a List<string>:

Session["myName"] = "Mike";
var myList = new List<string>{"a", "b", "c", "d"};
Session["myListString"] = myList;

An implicit cast exists between object and string, but not between object and List<string> so when you reference the session values, you need to cast:

var myName = Session["myName"];
var myList = (List<string>)Session["myList"];

You can set session variables anywhere in the application except in an AppStart file or within the Application_Start or Application_BeginRequest events in Global.asax. The Session_Start event is the earliest event available to you in the page life cycle. All data related to the session is stored in memory on the server, so it is not to be relied on. App Pool recycling and other events can cause sessions to terminate unexpectedly, so you should check to ensure that session variables are not null before trying to access their values. Or you can check them on each request - the PageStart file is a good mechanism for this:

if(Session["myName"] == null){
    Session["myName"] = "Mike";
}

If you placed the above code in a PageStart file in the root of the site, you can guarantee that Session["myName"] will be populated.

Because session variables are stored in memory on the server, you should be careful how much data you store in them. If you are on shared hosting, you will most likely be limited to the amount of memory that your application can consume, and the App Pool will be recycled to flush out anything stored in memory if you begin to exceed the limits set on your hosting plan. Session variables are a better place to store sensitive data such as passwords, but you should realise that sessions can be hijacked by malicious users (with a bit of work). You can encrypt the values for protection, or you can run your site under SSL which will take care of the encryption for you.

Application Variables

Application variables are also known as global variables. Once set, they are available to all users and pages. Just like session variables, application variables are stored in a collection of name/(dynamic)object pairs called AppState:

AppState["SiteName"] = "State Management in ASP.NET Web Pages";

An alternative syntax is available that makes use of dynamic properties:

App.SiteName = "State Management in ASP.NET Web Pages";

You can use these interchangeably, so if you set the value using the AppState[key] syntax, you can reference is using the App.DynamicProperty syntax within the site iteself. Usually, application variables are used to set constant values that relate to the site as a whole for all users. Consequently, you are most likely to set such values in AppStart, since this file is executed once at the time the application first starts.

The AppState or App objects are unique to the Web Pages framework although the backing store is not. It is usually referenced using Application[key] in the Web Forms framework, or the fully qualified HttpContext.Current.Application[key]. In fact, if you want to set global variables within Global.asax or within class files, you will have to use this longer syntax in a Web Pages site as the AppState or App objects are not available outside of cshtml files.

The sample site that accompanies this article illustrates setting a series of global variables in Global.asax and App_Start to calculate the execution order of various events within a Web Pages site.

void Application_Start(object sender, EventArgs e) 
{
    var counter = 1;
    HttpContext.Current.Application["Counter"] = counter;
    HttpContext.Current.Application["ApplicationStart"] = counter;

}

void Application_BeginRequest(object sender, EventArgs e)
{
    var counter = (int)HttpContext.Current.Application["Counter"]; 
    counter ++;
    HttpContext.Current.Application["Counter"] = counter;
    HttpContext.Current.Application["BeginRequest"] = counter;
}



void Session_Start(object sender, EventArgs e) 
{
    var counter = (int)HttpContext.Current.Application["Counter"]; 
    counter ++;
    HttpContext.Current.Application["Counter"] = counter;
    HttpContext.Current.Application["SessionStart"] = counter;

}

A variable is initialised in the Application_Start event with a value of one, and its value is stored in a global variable called Counter. At the same time, another global variable named ApplicationStart is given the value of Counter (1). The variable is referenced again in the Begin_Request event (which happens with every HTTP request), and explicitly cast to an int (since there is no implicit cast between object and int), and it has 1 added to whatever its value is, and the result is stored in a new global variable - BeginRequest. You can see the same thing happening in the Session_Start event (which happens when a new session is created). Similar code is also added to an AppStart file and a PageStart file:

[_AppStart.cshtml]
App.Counter ++; 
App.AppStart = App.Counter;
[_PageStart.cshtml]
App.Counter ++;
App.PageStart = App.Counter;

Notice that there is no need to explicitly cast the data type of the global variable. That's because the App property is dynamic, which means that data types are inferred from the context. If, for example, you attempt numeric operations against a dynamic object, it will be treated at runtime as a number.

Each of the global variables are printed out:

<table>
    <tr><th>Event</th><th>Order</th></tr>
    <tr><td>Application Start</td><td>@App.ApplicationStart</td></tr>
    <tr><td>AppStart Executed</td><td>@App.AppStart</td></tr>
    <tr><td>Begin Request</td><td>@App.BeginRequest</td></tr>
    <tr><td>Session Start</td><td>@App.SessionStart</td></tr>
    <tr><td>PageStart Executed</td><td>@App.PageStart</td></tr>
</table>

And the result clearly shows the order in which each event is fired.

Event Order
Application Start 1
AppStart Executed 2
Begin Request 3
Session Start 4
PageStart Executed 5

If you try this using the sample code in the download and get unexpected results - for example BeginRequest or PageStart result in higher numbers than 5, that's because you have run other pages before the one that demonstrates Application Variables. Just click the Restart button in WebMatrix to re-start the application then refresh the App.cshtml page. This helps to demonstrate that global variables are accessible from any page in the site and can be altered anywhere.

Cache

The Cache is primarily intended to be used to improve performance of web pages, in that you can add any arbitrary object to it and retrieve it at will. Cache items are stored in memory on the server, and can be considered as special global variables. In fact, pre-ASP.NET, Application state used to be treated as a kind of cache. The difference between Application variables and Cache is that Cache offers a management API. In Web Pages, cache is managed via a WebCache helper. When you want to add something to the Cache, you use the WebCache.Set method. This method takes four parameters: the name you want to give the cache item, the item itself, the number of minutes that the item should remain in the Cache (default: 20) and whether those minutes should restart everytime the item is referenced or not (default: true). This is called Sliding Expiration.

As an example, you might obtain weather forecasts from Yahoo to display on your site. These things don't change very often, but they can be time-consuming to retrieve as they require a request to be fired to Yahoo's service. This might slow you site down, so you really want to store the data you obtain and re-use it. Here's some code that does exactly that:

@using System.Xml.Linq;
@using System.Dynamic;
@{
    Page.Title = "Cache";

    var weather = WebCache.Get("weather");
    if(weather == null){
        XNamespace yweather = "http://xml.weather.yahoo.com/ns/rss/1.0";
        var xml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=26742853");
        var forecast = xml.Descendants(yweather + "forecast").FirstOrDefault();
        weather = new ExpandoObject();
        weather.high = (string)forecast.Attribute("high");
        weather.low = (string)forecast.Attribute("low");
        weather.conditions = (string)forecast.Attribute("text");
        weather.time = DateTime.Now;
        WebCache.Set("weather", weather, 720, false);
    }
}
<h1>Forecast for Rochester</h1>
<div>High: @weather.high &deg;F</div>
<div>Low: @weather.low &deg;F</div>
<div>Conditions: @weather.conditions</div>
<div>Cached at: @weather.time</div>

Yahoo's weather API return XML (RSS), so the Linq To Xml library is referenced so that I can use it to work with Yahoo's data more easily. I first attempt to obtain an item from the Cache called "weather" using the WebCache.Get helper method. If that item doesn't exist (is null), I obtain the XML from Yahoo using the XDocument.Load method and then use the Linq To XML API to query the XML and obtain the values I am after. I assign these as properties of a dynamic object, which I then store for 12 hours in the Cache using the WebCache.Set helper method described earlier. I set slidingExpiration to false, because I want the item to be flushed from the Cache in 12 hours regardless.

Since cached items are stored in memory on the server, they are volatile and may be cleared before you expect if the memory is reclaimed by the server. So just like session and application level variables, you should not assume that they exist just because your code put them there.

All of the concepts presented here are featured in the sample site which is available as a GitHub repo.