Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
Markdown Monster - The Markdown Editor for Windows

A small, intra-app Object to String Serializer


:P
On this page:

Here's a scenario that keeps popping up for me occasionally: In a number of situations I've needed to quickly and compactly serialize an object to a string. There are a million serializers out there from XML to binary, to JSON and even the somewhat compact LosFormatter in ASP.NET. However, all the built-in serialization mechanisms are pretty bloated and verbose especially when sending output to strings.

An Example for Micro-Serialization

Some scenarios where this makes sense is for compact configuration storage or token generation for application level storage. A common place where I need this is for cookies that pack a few values together or for a forms authentication ticket value. For example, I have a UserState object with common properties that stores UserId, User Name, Display Name, email address and a security level in a small object that is carried through with the forms authentication cookie - this avoids having to hit the database (or Session which I don't use) for user information on every hit.

The UserState object is very small and so it seems like overkill to throw a full JSON or XML serialization at it. Furthermore the XML and JSON sizes are pretty large. For my UserState object I used to actually hand code a 'custom' serialization mechanism which is trivial simple and based on string.Split() and string.Join() with a pipe symbol.

To demonstrate that scenario - which I have implemented on numerous other occasion - here's the code:

/// 
/// Exports a short string list of Id, Email, Name separated by |
/// 
public override string ToString()
{            
    return string.Join(STR_Seperator, new string[] { 
                                             UserId,
                                             Name,
                                             IsAdmin.ToString(),
                                             Email }); } /// /// Imports Id, Email and Name from a | separated string /// public bool FromString(string itemString) { if (string.IsNullOrEmpty(itemString)) return false; var state = CreateFromString(itemString); if (state == null) return false; UserId = state.UserId; Email = state.Email; Name = state.Name; IsAdmin = state.IsAdmin; return true; } /// /// Creates an instance of a userstate object from serialized /// data. /// /// IsEmpty() will return true if data was not loaded. A /// UserData object is always returned. /// public static UserState CreateFromString(string userData) { if (string.IsNullOrEmpty(userData)) return null;
string[] strings = userData.Split(new string[1] {STR_Seperator}, StringSplitOptions.None ); if (strings.Length < 4) return null; var userState = new UserState(); userState.UserId = strings[0]; userState.Name = strings[1]; userState.IsAdmin = strings[2] == "True"; userState.Email = strings[3]; return userState; }

Simple enough, but it ends being a bit of non-flexible code as the properties have to be stored and retrieved in exactly the right order. Also if the class adds a property I have to manually add any properties in both ToString() and CreateFromString(). Nevertheless, this is efficient since it is handcoded and I've written code like this on a number of occasions, often enough to warrant a more generic solution.

Making it Generic: StringSerializer

Besides violating the DRY priniciple, there's also the issue of this code not being very flexible. If I decide to add a property to this object the serialization routines have to be updated. Or - more likely perhaps in an object like this, if it's subclassed there's then no easy way to add additional properties to serialize.

So today I spent a little time creating a simple generic component that basically provides this behavior as a reusable component. Here's an implementation of a simple StringSerializer:

///

/// A very simple flat object serializer that can be used /// for intra application serialization. It creates a very compact /// positional string of properties. /// Only serializes top level properties, with no nesting support /// and only simple properties or those with a type converter are /// supported. Complex properties or non-two way type convertered /// values are ignored. /// /// Creates strings in the format of: /// Rick|rstrahl@west-wind.com|1|True|3/29/2013 1:32:31 PM|1 /// /// /// This component is meant for intra application serialization of /// very compact objects. A common use case is for state serialization /// for cookies or a Forms Authentication ticket to minimize the amount /// of space used - the output produced here contains only the actual /// data, no property info or validation like other serialization formats. /// Use only on small objects when size and speed matter otherwise use /// a JSON/XML/Binary serializer or the ASP.NET LosFormatter object. /// public static class StringSerializer { private const string Seperator_Replace_String = "-@-"; /// /// Serializes a flat object's properties into a String /// separated by a separator character/string. Only /// top level properties are serialized. /// /// /// Only serializes top level properties, with no nesting support /// and only simple properties or those with a type converter are /// 'serialized'. All other property types use ToString(). /// /// The object to serialize /// Optional separator character or string. Default is | /// public static string SerializeObject(object objectToSerialize, string separator = null) { if (separator == null) separator = "|"; if (objectToSerialize == null) return "null"; var properties = objectToSerialize.GetType()
.GetProperties(BindingFlags.Instance |
BindingFlags.Public); var values = new List<string>(); for (int i = 0; i < properties.Length; i++) { var pi = properties[i]; // don't store read/write-only data if (!pi.CanRead && !pi.CanWrite) continue; object value = pi.GetValue(objectToSerialize, null); string stringValue = "null"; if (value != null) { if (value is string) { stringValue = (string)value; if (stringValue.Contains(separator)) stringValue = stringValue.Replace(separator, Seperator_Replace_String); } else stringValue = ReflectionUtils.TypedValueToString(value, unsupportedReturn: "null"); } values.Add(stringValue); } if (values.Count < 0) // empty object (no properties) return string.Empty; return string.Join(separator, values.ToArray()); } /// /// Deserializes an object previously serialized by SerializeObject. /// /// /// /// /// public static object DeserializeObject(string serialized, Type type, string separator = null) { if (serialized == "null") return null; if (separator == null) separator = "|"; object inst = ReflectionUtils.CreateInstanceFromType(type); var properties = inst.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public); string[] tokens = serialized.Split(new string[] { separator }, StringSplitOptions.None); for (int i = 0; i < properties.Length; i++) { string token = tokens[i]; var prop = properties[i]; // don't store read/write-only data if (!prop.CanRead && !prop.CanWrite) continue; token = token.Replace(Seperator_Replace_String, separator); object value = null; if (token != null) { try { value = ReflectionUtils.StringToTypedValue(token, prop.PropertyType); } catch (InvalidCastException ex) { // skip over unsupported types } } prop.SetValue(inst, value, null); } return inst; } /// /// Deserializes an object serialized with SerializeObject. /// /// /// /// /// public static T Deserialize(string serialized, string separator = null) where T : class, new() { return DeserializeObject(serialized, typeof(T), separator) as T; } }

Note that there are two dependencies on ReflectionUtils and the TypedValueToString and StringToTypedValue which handle the string type conversions. They're included in a support file linked at the bottom of this post.

With this object in place I can now re-write the UserState object's built in self serialization like this:

public override string ToString()
{
    return StringSerializer.SerializeObject(this);
}

public bool FromString(string itemString)
{
    if (string.IsNullOrEmpty(itemString))
        return false;

    var state = CreateFromString(itemString);
    if (state == null)
        return false;

    // copy the properties
    DataUtils.CopyObjectData(state, this);

    return true;
}

public static UserState CreateFromString(string userData)
{
    if (string.IsNullOrEmpty(userData))
        return null;

    return StringSerializer.Deserialize<UserState>(userData);
}


public static UserState CreateFromFormsAuthTicket()
{
    return CreateFromString(((FormsIdentity)HttpContext.Current.User.Identity).Ticket.UserData);
}

The object now is uses the single StringSerializer methods to serialize and deserialize and the object is now more flexible in that it can automatically deal with additional properties that are added, perhaps even in a subclass.

To use the UserState object is super simple now. For example, in my base MVC Controller I can now easily attach the UserState object to the controller and the ViewBag:

protected override void Initialize(RequestContext requestContext)
{
    base.Initialize(requestContext);

    // Grab the user's login information from FormsAuth            
    if (this.User.Identity != null && this.User.Identity is FormsIdentity)
        this.UserState = UserState.CreateFromFormsAuthTicket();
    else
        this.UserState = new UserState();
                        
    // have to explicitly add this so Master can see untyped value
    ViewBag.UserSate = this.UserState;
    ViewBag.ErrorDisplay = this.ErrorDisplay;
}

making the UserState easily available anywhere in the app. The user data is written once when  user logs or changes his profile info but otherwise the UserState is only read on each hit and made available to the app when logged in users are present. It's a great way to store basic information about a user without having to hit the database (as you have to with MemberShip in ASP.NET).

Limitations

As the comments describe, StringSerializer is not meant to be a full featured serializer. If you need to serialize large and nested objects or you need to share the data with other applications, this is probably not what you want to use. Use the JSON.NET serializer (because it's fast, robust and reasonably compact) or the Xml Serializer instread. StringSerializer has no support for nested objects and it only works the built-in .NET system types and anything that supports two-way ValueConverter string conversion.

StringSerializer makes a few assumptions: It requires that the fields serialized are desterilized in exactly the same order - IOW, deserialization expects the type signature on the other end to be the same as the input. Since there are no property names or any other kind of meta-data in the output, the data is small, but also is set up in a fixed format that must be the same on both ends. If you add properties the format breaks - hence the point about intra-app serialization - it's not meant as a portable format to share amongst machines or platforms or be version aware.

When it makes Sense

This serializer is fast and produces very compact output for small, flat objects and if all of these are important then it's a good fit. Serializing the 5 fields as I'm doing in the UserState object using JSON.NET would be overkill, produce a larger string and take more time to process.

The string data created is small - my UserState object serialized with this is 85 bytes in length which is basically the string value representation for each property plus the separators. JSON.NET formatted that same object to 166 bytes. The Xml Serializer to 379. The LosFormatter created a whopping 604 bytes (and required [Serializable]). StringSerializer also runs faster than any of the others I tried - a 1000 iterations on my machine take 5ms for the StringSerializer, 8ms for JSON.NET, 11ms for the XML Serializer and 16 for the LosFormatter . The perf of all of these is small enough to not matter, but the output size is significantly smaller for the StringSerializer which in the case of cookie or FormsAuth usage is vital.

It's not an everyday kind of component, but rather fits a special use case scenario, but one that I've run into often enough to warrant creating a reusable class for. Anyway I hope some of you find this useful.

Resources

Posted in ASP.NET  .NET  C#  

The Voices of Reason


 

Erick
April 01, 2013

# re: A small, intra-app Object to String Serializer

I just had to do something very similar with UserState and the forms auth ticket (because like you I don't use session, and felt the DB hit was excessive). I had never really done a custom forms auth ticket before, and wasn't sure if going this route was a good idea or not. Nice to see another person with experience doing the same thing. Makes me feel better.

I didn't create a generic re-use library like you did, but ended up with a format similar to your end result. Throughout the whole process though, I was thinking it might be wiser to use JSON for it, since it's basically baked into the framework these days. However I was concerned about cookie size.

Anyways, Ill check this util out, either ways it's good to see confirmation of sound principals. Thanks.

BZ
April 03, 2013

# re: A small, intra-app Object to String Serializer

I use a different approach. What I do is load the data for the identity from a repository. But the repository has a caching mechanism. With caching I realistically only hit the database once. There is an added benefit in that it is easy to reset the cache for an item if say the application let's the user change some of the data for the identity for example email, first name, last name is pretty common.

Rick Strahl
April 03, 2013

# re: A small, intra-app Object to String Serializer

@BZ - Yes that works reasonably well if your user count is low, but otherwise you end up caching a large number of users (or all users)? If you have a busy open internet site though where you're tracking users that could be a problem. With Cookie/FormsAuth data the client handles the payload caching.

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024