Headers - attaching arbitrary properties to objects
On a project recently I had a marker interface like this:
public interface IMessage
{
}
I wanted the ability to attach arbitrary headers to a message - for example:
message.SetHeader("Expires", "2011-09-03 15:30:00");
message.SetHeader("CorrelationId", 45);
I didn't want SetHeader
to be a method on the interface, so I created an extension method. But how can we store the arbitrary header values against the object instance?
My solution was to use ConditionalWeakTable
. This allows properties to be stored against objects without preventing the object from being garbage collected.
The implementation looked something like this:
public static class MessageExtensions
{
private static readonly ConditionalWeakTable<IMessage, HeaderCollection> headerMap = new ConditionalWeakTable<IMessage, HeaderCollection>();
public static void SetHeader(this IMessage message, string header, string value)
{
var headers = GetHeaders(message);
headers[header] = value;
}
public static string GetHeader(this IMessage message, string header)
{
var headers = message.GetHeaders();
string value;
headers.TryGetValue(header, out value);
return value;
}
public static HeaderCollection GetHeaders(this IMessage message)
{
return headerMap.GetValue(message, x => new HeaderCollection());
}
}
I put the extension methods in the global namespace, so that they are available on any IMessage
without the coder needing to include the namespace.
Here is a unit test to demonstrate the garbage collection continuing to work:
[TestMethod]
public void HeadersDoNotPreventGarbageCollection()
{
WeakReference reference = null;
new Action(() =>
{
var message1 = new MessageA();
message1.SetHeader("Test", "Foo");
reference = new WeakReference(message1, true);
Assert.IsNotNull(reference.Target);
})();
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.IsNull(reference.Target);
}