Extension methods + weak references = extension pseudo-properties in C#

Extension methods can be helpful for adding functionality onto existing classes, especially where one doesn’t have the ability to control a class definition and thus can’t add the methods directly. Adding extension properties to C# could be just as useful in some programming scenarios, but so far isn’t slated for definite release. Yet when working with third-party code such as the Sitecore API, it can be especially useful to add on both post-hoc functionality and state. And while extension methods can’t precisely duplicate the syntax of properties in C#, they can come close through the use of getter/setter methods as in Java, if some facility is used to store data on the object.

The System.Runtime.CompilerServices.ConditionalWeakTable class is ideal for such use, as a collection specifically made to contain weak references, i.e. to allow any referred-to object to be garbage collected (releasing the weak reference as well) if all strong references have been removed. What this means on a practical basis is that one can provide ancillary state for an object by using such a weak-reference collection in a static, and ideally thread-safe, way. It’s exactly what’s needed to dummy up extension properties in C#, while we wait for the real deal from Microsoft.

Here’s a basic example:

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

public static class ExtendedDataExtensions
{
    ///<summary>Stores extended data for objects</summary>
    private static ConditionalWeakTable<object, object> extendedData = new ConditionalWeakTable<object, object>();

    /// <summary>
    /// Creates a collection of extended pseudo-property values
    /// </summary>
    /// <param name="o">The object to receive the tacked-on data values</param>
    /// <returns>A new dictionary</returns>
    internal static IDictionary<string, object> CreateObjectExtendedDataCache(object o)
    {
        return new Dictionary<string, object>();
    }

    /// <summary>
    /// Sets an extended pseudo-property value on this object
    /// </summary>
    /// <param name="o">this object</param>
    /// <param name="name">The pseudo-property name</param>
    /// <param name="value">The value to set (if null, any value for the name will be removed)</param>
    public static void SetExtendedDataValue(this object o, string name, object value)
    {
        if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Invalid name");
        name = name.Trim();

        // Gets the key-value collection serving as extended "properties" for this object
        IDictionary<string, object> values = (IDictionary<string, object>)extendedData.GetValue(o, CreateObjectExtendedDataCache);

        if (value != null)
            values[name] = value;
        else
            values.Remove(name);
    }

    /// <summary>
    /// Gets a pseudo-property value stored for this object
    /// </summary>
    /// <typeparam name="T">The type to return</typeparam>
    /// <param name="o">this object</param>
    /// <param name="name">The pseudo-property name</param>
    /// <returns>A value of the indicated type, or the type default if not found or of a different type</returns>
    public static T GetExtendedDataValue<T>(this object o, string name)
    {
        if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Invalid name");
        name = name.Trim();

        IDictionary<string, object> values = (IDictionary<string, object>)extendedData.GetValue(o, CreateObjectExtendedDataCache);
        object value = null;
        if (values.TryGetValue(name, out value)) 
        {
            if (value is T)
                return (T)value;
            else
                return default(T); // or throw an exception, as desired
        }
        else
            return default(T);
    }

    /// <summary>
    /// Gets a pseudo-property value stored for this object
    /// </summary>
    /// <param name="o">this object</param>
    /// <param name="name">The pseudo-property name</param>
    /// <returns>A value if found for the specified name, otherwise null</returns>
    public static object GetExtendedDataValue(this object o, string name)
    {
        if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Invalid name");
        name = name.Trim();

        IDictionary<string, object> values = (IDictionary<string, object>)extendedData.GetValue(o, CreateObjectExtendedDataCache);
        object value = null;
        if (values.TryGetValue(name, out value))
            return value;
        else
            return null;
    }
}

A brief example of calling the methods:

Item item = Sitecore.Context.Item;
item.SetExtendedDataValue("Tweet Count", 300);
// ...
int tweetCount = item.GetExtendedDataValue<int>("Tweet Count");

… after which one’s thoughts naturally turn to wrapping things up a bit more nicely, seasoning to taste for such issues as null and range checking. An inane example, for want of a better one:

public static class ItemExtensions
{
    public static int GetTweetCount(this Item item)
    {
        return item.GetExtendedDataValue<int>("Tweet Count");
    }

    public static void SetTweetCount(this Item item, int tweetCount)
    {
        item.SetExtendedDataValue("Tweet Count", tweetCount);
    }
}

And, of course, if it’s desired to make the pseudo-properties thread-safe, merely replace the dictionary storage with ConcurrentDictionary, with nearly identical performance.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s