One of the big issues that are always raised whenever I go into a project is the size of ViewState on complex pages. I'm not talking the 200 - 500 byte ViewState that a simple page might have, but the 300K - 1mb that a very large, complex grid might have. The other day I was looking at something in Reflector and out of the corner of my eye I saw the System.IO.Compression namespace. That's something I rarely, if ever, use in web applications but it made me think what could be compressed in a web app. First thing that came to mind is that huge ViewState. As is usually the case when working with ASP.Net, this is relatively easy to accomplish with two or three classes and a few methods.

Method of attack:

Whenever I'm doing something like this where I want to override some built in functionality with my own, I dive into Reflector and figure out how to go about plugging in my functionality. In this case, I found two routes. First, by overriding the PageStatePersister property of my base page, I can override the PageStatePersister used to store the data. The other option is to use a PageAdapter to supply a PageStatePersister. Digging deeper I decided I really want to keep all the functionality from the built in PageStatePersister implementations (HiddenFieldPageStatePersister and SessionPageStatePersister).

Looking at the way the persisters work, it became clear that the easiest point of injection would be to replace the IStateFormatter instance with one that chains to the built in one. Something akin to the way we chain streams together to add functionality. The only problem here is that there is no way to set the StateFormatter as the property is not virtual and the field is private. Quick bit of reflection took care of that though.

IStateFormatter:

The IStateFormatter interface is exceedingly simple. Two methods:

    public interface IStateFormatter
    {
        object Deserialize(string serializedState);
        string Serialize(object state);
    }

With the chaining in place, the shell of my class looks like:

    public class CompressedStateFormatter : IStateFormatter
    {
        IStateFormatter nextFormatter = null;

        public CompressedStateFormatter(IStateFormatter nextFormatter)
        {
            this.nextFormatter = nextFormatter;
        }

        public object Deserialize(string serializedState)
        {
            string compressedState = serializedState;
            string decompressedState;

            // Decompression logic goes here.

            return nextFormatter.Deserialize(decompressedState);
        }

        public string Serialize(object state)
        {
            string decompressedState = nextFormatter.Serialize(state);
            string compressedState;

            // Compression logic goes here.

            return compressedState;
        }
    }

Compression: 

My initial, simplistic, stab at the compression logic looked like this:

  using (MemoryStream memStream = new MemoryStream())
  {
CompressionMode mode = CompressionMode.Compress; using (Stream compStream = new GZipStream(memStream, mode)) { using (StreamWriter writer = new StreamWriter(compStream)) { writer.Write(decompressedState); } }
    byte[] compData = memStream.ToArray();
    compressedState = Convert.ToBase64String(compData);
  }

Very simple and straight forward.

Note: One problem I ran into was that I grabbed the data from compressedStream before closing compressionStream, which meant the GZip footer was not included in the data, causing it to fail decompression. The failure during decompression was not, as I had expected, an exception stating that the data was bad. Instead, the decompression stream was just empty. So, if anyone runs into an issue where they compress something and then try to decompress it but can't get any data out of the decompression stream, that may be the cause.

Doing a bit of analysis on this, I found that for a very small ViewState, less than about 2Kb, the compression would actually increase the amount of data. To combat this, I check after the compression occurs whether it was useful, and only then use it. To let the decompression side know if it should decompress, I'm using a 1 character marker on the front of the output to indicate compression:

  using (MemoryStream memStream = new MemoryStream())
  {
CompressionMode mode = CompressionMode.Compress; using (Stream compStream = new GZipStream(memStream, mode)) { using (StreamWriter writer = new StreamWriter(compStream)) { writer.Write(decompressedState); } }
byte[] compressedData = compressedStream.ToArray(); compressedState = Convert.ToBase64String(compressedData); }
  if (compressedState.Length < decompressedState.Length)
  {
    compressedState = "1" + compressedState;
  }
  else
  {
    compressedState = "0" + decompressedState;
  }

Thus, in the worst case, this adds 1 character to the ViewState output.

Decompression:

Similar to compression, the decompression code is very simple:

  bool compressed = compressedState.StartsWith("1");

  compressedState = compressedState.Substring(1);

  if (compressed)
  {
byte[] data = Convert.FromBase64String(compressedState); using (MemoryStream memStream = new MemoryStream(data)) {
CompressionMode mode = CompressionMode.Decompress; using (Stream compStream = new GZipStream(memStream, mode))
{ using (StreamReader reader = new StreamReader(compStream)) { decompressedState = reader.ReadToEnd(); } } } } else { decompressedState = compressedState; }

Plugging this in:

I mentioned above that I had to use a bit of reflection to plug my CompressedStateFormatter into the PageStatePersister. This is really quite simple:

  Type type = typeof(PageStatePersister);


BindingFlags bind = BindingFlags.NonPublic | BindingFlags.Instance;
FieldInfo field = type.GetField("_stateFormatter", bind); CompressedStateFormatter formatter =
new CompressedStateFormatter(base.StateFormatter); stateFormatterField.SetValue(this, formatter);

Note: As with all reflection this is a point of maintenance when upgrading to the next version of .NET as the instance field may change.

This code was then placed into the constructor of two derived PageStatePersister implementations:

  public class CompressedHiddenFieldPersister
: HiddenFieldPageStatePersister { public CompressedHiddenFieldPersister(Page page) : base(page) {
// Wrapping logic goes here.
} } public class CompressedSessionPersister
: SessionPageStatePersister { public CompressedSessionPersister(Page page) : base(page) {
// Wrapping logic goes here.
}
}

Now, depending on where I want the ViewState stored, I can also choose to store a compressed version.

As I noted above, adding this to the page is a matter of either overriding the PageStatePersister property:

  private PageStatePersister persistor;
  protected override PageStatePersister PageStatePersister
  {
    get
    {
      if (this.persistor == null)
        this.persistor = 
new CompressedHiddenFieldPersister(this); return this.persistor; } }

Or using a PageAdapter:

  public class PageAdapter : System.Web.UI.Adapters.PageAdapter
  {
    public override PageStatePersister GetStatePersister()
    {
      return new CompressedSessionPersister(base.Page);
    }
  }

Which is applied with a very generic .browser file:

  <browsers>
    <browser refID="IE6to9">
      <controlAdapters>
        <adapter controlType="System.Web.UI.Page"
                 adapterType="Schwab.SI.Common.Web.PageAdapter" />
      </controlAdapters>
    </browser>

    <browser refID="MozillaFirefox">
      <controlAdapters>
        <adapter controlType="System.Web.UI.Page"
                 adapterType="Schwab.SI.Common.Web.PageAdapter" />
      </controlAdapters>
    </browser>
  </browsers>

Conclusion:

Having just developed this on Friday on my way out the door I haven’t done too much testing of the performance implications or the actual space savings. However, preliminary testing showed that a ViewState of 800+Kb was compressed down to 300Kb or so. Similarly, a simpler ViewState of 10K is reduced to 3K.

Now, obviously, ViewState compression is only really useful when HTTP compression is not applicable, which could happen in any of the following (off the top of my head):

  • Server does not support HTTP compression
  • Application developer does not have rights to enable compression
  • When the ViewState is not to be sent through he HTTP stream (when stored in Session or DB perhaps)

So, when any of those may be the case, it might be useful to run the ViewState through a compression stream.