Frictionless data persistence in ASP.NET WebForms
Share
Sometimes you come across a riff that really strikes a chord; for me, this happened when I read an older article demonstrating using attributes to declare where fields on an ASP.NET page persist; in the Application store, Context, or ViewState. I think the value of this approach is that it allows a developer to treat their Page and UserControl members as plain properties, and then declare how those properties will persist without having to write implementation-specific code. Even at its most elegant, saving a value to one of the many persistent targets might look something like this:
public int Foo
{
get { return (int)(ViewState["Foo"] ?? -1); }
set { ViewState["Foo"] = value; }
}
The code above must be repeated for every property, and changed whenever the intended target changes. In addition, any processing applied on the value must be called explicitly: you’re on your own. By using a declarative approach, we can write this:
[PersistentValue("Foo", PersistentValueTargets.ViewStateClient)]
public int Foo { get; set; }
And if we want to try a different approach, we could do this, without changing any class code:
[PersistentValue("Foo",
PersistentValueTargets.Cache,
PersistentValueOptions.Encrypted)]
public int Foo { get; set; }
In the last example, we changed the behavior of Foo so that it is stored in the ASP.NET Cache rather than in ViewState, and we added an additional option that encrypts the value using the Data Protection API before doing so. We’re going to build this declarative framework so that we have a complete menu from which to store values, and a few handy options.
Our available targets are Application, Cache, Context, Cookies, Hidden Fields, Query Strings, Session, ViewStateClient, and ViewStateServer. Our available options are Encoded, Encrypted, Compressed, and EncryptedAndCompressed which allow us to experiment with performance and security, again without additional work. Combining the ViewStateServer target with the Compressed option will provide efficient storage of the ViewState on the server, while conserving memory at the same time.
Keep in mind that you still need to use your chosen target as it was intended. For example, storing items in the current HttpRequest Context will only persist those items for the lifetime of the request, and no further. This target is usually reserved for situations where you may have multiple UserControls that all require the same data, and it would be extremely inefficient to store a separate field for each instance when you can simply place one item on the Request and retrieve that value for every instance. Similarly, the Application target stores values that are not unique across requests.
Creating the attribute
The first step is to create a custom attribute that contains a persistent target and processing option that we can apply to property members of a class. The attribute also provides a user-provided key to distinguish the persisted value from other stored values in the target dictionary.
using System;
using Dimebrain.Web.Code;
namespace Dimebrain.Web.Code
{
[AttributeUsage(AttributeTargets.Property)]
public class PersistentValueAttribute : Attribute
{
public PersistentValueTargets Target { get; set; }
public PersistentValueOptions Options { get; set; }
public string Key { get; set; }
public PersistentValueAttribute(string key, PersistentValueTargets target, PersistentValueOptions options)
{
Target = target;
Options = options;
Key = key;
}
public PersistentValueAttribute(string key, PersistentValueTargets target)
: this(key, target, PersistentValueOptions.None)
{ }
}
}
Handling the attribute
Next we will write a set of extension methods that provide new methods for the Page class to save and load persistent values based on the new attribute. The public methods will take the current page and the page’s ViewState instance (which is a StateBag object under the hood) and perform the on-demand persistence of the values.
public static void SavePersistentValues(this Page page, object state)
{
var flags = BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance;
var properties = page.GetType().GetProperties(flags);
var serverStateKeys = new List();
foreach(var property in properties)
{
if (property == null)
{
continue;
}
var attributes = property.GetCustomAttributes(typeof(PersistentValueAttribute), true);
foreach (var attribute in attributes)
{
SavePersistentValue(page, state, property, attribute, serverStateKeys);
}
}
}
public static void LoadPersistentValues(this Page page, StateBag viewState, object baseState)
{
var flags = BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance;
var properties = page.GetType().GetProperties(flags);
// Retrieve the client-side view state
var clientState = ((Pair)((Pair)baseState).First).Second as ArrayList;
viewState.Populate(clientState);
foreach (var property in properties)
{
if (property == null)
{
continue;
}
var attributes = property.GetCustomAttributes(typeof(PersistentValueAttribute), true);
foreach (var attribute in attributes)
{
// Rehydrate the value
var value = LoadPersistentValue(page, attribute, viewState);
if (value == null)
{
// Skip uninitialized values
continue;
}
// Convert to property type if needed (i.e. Cookie's value is always a string)
var converter = TypeDescriptor.GetConverter(property.PropertyType);
if(value.GetType() != property.PropertyType)
{
value = converter.ConvertFrom(value);
}
// Set the property value
property.SetValue(page, value, BindingFlags.SetProperty, null, null, null);
// Sanity check
var expected = property.GetValue(page, BindingFlags.GetProperty, null, null, null);
Debug.Assert(expected.Equals(value), "Did not retrieve the loaded value from the property after setting it!");
}
}
}
Targets and options
We haven’t demonstrated the implementation-specific bits for persistence targets and options. An example of a target strategy is the code that saves your property in the Request’s query string:
using System.Web.UI;
using Dimebrain.Web.Code;
using Dimebrain.Web.Helpers;
namespace Dimebrain.Web.Extensions
{
public static partial class PersistentValueExtensions
{
private static void ToQueryString(PersistentValueAttribute persist, object value)
{
// Wrap the query string to make it writable
using (var qs = new WritableQueryString())
{
var query = qs[persist.Key];
var queryValue = value.ToString();
if (query == null)
{
qs.Add(persist.Key, queryValue);
}
else
{
qs[persist.Key] = queryValue;
}
}
}
private static object FromQueryString(Page page, PersistentValueAttribute persist)
{
return page.Request.QueryString[persist.Key];
}
}
}
This a flexible and powerful way to manage the multiple ways we have to persist state in ASP.NET WebForms without constantly writing and rewriting boilerplate code. The most difficult aspect of building this framework was providing a way for the persistence functions to know the difference between client and server ViewState since the Page itself only knows about state on the client. Without this, the ViewState stored on the server would always duplicate on the client, defeating the purpose.
Downloads: VS2008
Socialized