Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

C# Easy Extension Properties

0.00/5 (No votes)
30 May 2012 1  
(v2) How to convince your C# objects to carry extended properties, in real time, and with no modifications in their code. V2 includes a collector for disposable properties.

Introduction

As it is well known, C# 4.0 does provide support for what are called "Extension Methods". They are basically a syntactic sugar supported by the compiler so that it helps you believe, syntactically, that you have "extended" the type of the instance with more methods, without inherit from it, and even if the type itself is sealed.

But as it is today, there is no support for "Extension Properties". There are many scenarios where having such capability would be extremely useful, basically any situation where you need some objects to carry with them some information that you generate at run time - regardless of their types, or if they are sealed or not, or even if you have access to its source code or not.

Till now, those situations were solved creating some sort of wrapping that inherits from the object’s type, using proxy factories (á la AOP/IoC), mixins, reflection and emit, dynamics, and the like. Those are extremely powerful alternatives, and the Internet has plenty of information that permits you to build your own library, or use any of the available ones. But also those are typically complex and heavy weighted, and they tend to have a substantial impact on performance. And when you have to deal with an unknown number of different types, potentially big, then there would be a problem trying to generate those wrappers using reflection and emit at run time…

Now, I have always had the conviction that there was already a mechanism for adding properties to instances on the fly, without having to create a wrapper to extend the type and instantiating a proxy: you only need to use a visual designer to see that, almost for any control it is using, it shows more "properties" than the ones the original control had.

Indeed it exists: it is based on the TypeDescriptor class. Among many other nice things, it will permit us to attach an attribute to any class instance at run time – and note that it does not mean it would modify the type’s structure in any way: it just add this new attribute to an internal list of attributes the instance is carrying with it.

Using this fact, we will use a custom attribute to store the information we need. Unlike the standard properties, which are fixed in the type’s metadata when it is written (or dynamically created), we are going to define this extended properties as key/value pairs that you can attach to any given object at run time, so that they can be accessed later to set or retrieve the value they store, as we do with the standard properties of any object. The pairs will be stored in the instance of the custom attribute we will attach on-demand to the object instance. Then we will build some methods that will permit us access to those contents in an easy way. Note that the solution is just emulation, as it cannot mimic completely the syntax of native properties - and this is why I prefer to call them "meta properties".

Using the Solution

Before digging deeper into the details of the solution, let’s take a look at how to use it. Let’s suppose we have an object instance we want to extend with a new meta-property called "MyNewProperty", and we want to set it with a string value, and later retrieve it. All the magic is as easy as just doing the following:

using MB.Tools;
···
  // Instance is of whatever type, as far it is a class, and not a struct or enum
  instance.SetMetaProperty( "MyNewProperty", "James Bond" );
  ···
  var value = instance.GetMetaProperty( "MyNewProperty" );
  Console.WriteLine( "Value: {0}", value );
···
  • The extension method SetMetaProperty(name, value) is used to create a meta-property attaching it to the host instance it is called upon, using a name and a value to store in it. If the meta-property already exists, its former value is substituted with the new one, and this former value is disposed if possible (more on this later).
  • The second main extension method is GetMetaProperty(name). It returns the value stored in the meta-property whose name is given, or throws an exception if such meta-property does not exist.

Note that the property names are just mere strings, and the only restrictions I chose to intercept are that they cannot be null or empty. But unlike the standard properties, they accept any string as its name - internally, these strings are used as the keys to locate the meta-property. Also note that, in any case, they are considered case sensitive.

More Useful Methods

There are some more extension methods (that as happens with the methods above also extend the type System.Object):

  • bool TryGetMetaProperty(name, out value): is used as expected to return true only if the meta-property exists, and setting its value in the out parameter. If the meta-property does not exist, it just returns false without throwing an exception.
  • bool HasMetaProperty(name): returns true if the object it is invoked upon carries a meta-property with the name given, or false otherwise.
  • IMetaProperty RemoveMetaProperty(name): is used to remove the meta-property from the object it is invoked upon, returning null if it is not found, or an IMetaProperty instance that can be used to access its contents and status. Its own properties are AutoDispose, PropertyName and PropertyValue.
  • void ClearMetaProperties(): is used to remove all the meta-properties the object it is invoked upon may carry, and potentially disposing them.
  • List<string> ListMetaProperties()</string>: returns a list, potentially empty, with the names of the meta-properties the object it is invoked upon may carry.

The following are not extension methods, but static ones of the static class MetaPropertyExtender:

  • IMetaPropertiesHolder GetMetaPropertiesHolder( obj, bool create): returns the attribute instance attached to the host object, in the form of an object that implements the IMetaPropertiesHolder interface. This interfaces provides the IEnumerable<IMetaProperty> MetaProperties property which permits you iterate though the meta-properties the host object may carry with it.

How It Works (The Basics)

As mentioned in the introduction, we will use a custom attribute that will be attached to the host object as needed using the TypeDescriptor mechanism. This custom attribute is an internal class that implements the IMetaPropertiesHolder interface, and note that, as mentioned before, the GetMetaPropertiesHolder() method permits you to obtain the one carried with your host instance, in case you need it. This custom attribute will provide the capability to host the key/value pairs we will need to support the meta-properties. Your can access them directly using its MetaProperties property.

When needed, typically because you are setting a meta-property using SetMetaProperty(), an instance of the custom attribute class is created and attached to the host object using the AddAttributes() method of TypeDescriptor. In this case, a first meta-property is always created, named "TypeDescriptionProvider", to store the descriptor provider returned when adding the custom attribute. It has no use in this solution, but it might have in the future.

When retrieving the value stored in the meta-property, typically by using GetMetaProperty() or TryGetMetaProperty(), the list of extended attributes of the host instance is obtained using the GetAttributes() method of TypeDescriptor, and locating the first attribute who is an instance of the custom attribute class. If there is none, the first Get() method will throw an exception, whereas the second TryGet() method just merely returns false.

Once you have retrieved this holder, it is just a matter of manipulating its internal key/value pairs store, to provide the extension methods that creates the illusion of having extended meta-properties.

The real code is, obviously, more complex, as it intercepts a number of errors throwing the exceptions needed, and a number of methods have been refactored for simplicity. Also, in this V2, the code has been modified to take into account what is explained in the next section.

A Note of Caution

It has been mentioned before, but I would like to emphasize that this solution can only extend instances of classes. It does not work with structs and enums.

On the good side, it works with any instance of any class without requiring you to do any special thing. Once a reference to the namespace is set, it is just a matter of using the extension methods.

Using the Examples Provided

Please uncomment the "#undef DEBUG" directive if you don’t want to see the traces and debug information.

What's New in Version 2: Disposable Scenarios

When the host instance is finalized, its metadata is finalized as well. But without using proxies or interceptors, there is no way to guarantee we will be informed when a given instance is disposed. In this case, the standard, simplest and safest rule is not to store objects that need to be disposed in the extended meta-properties.

Is this a strong limitation? Well, it really depends on your scenario. For instance, instead of storing a Connection object, why not just try to store its ConnectionString property, which is not an unmanaged resource we need to dispose when it is no longer needed? If this is your case, you can go with the solution as it is today without using the optional mechanism described below.

So, now, what if you have no other choice but to store in the meta-properties objects that will need to be disposed later? Typically, when its host object is disposed.

Version 2 of MetaPropertyExtender implements an optional mechanism to help you achieve something similar. Basically, it consist in:

  1. a list of WeakReferences that tracks those objects that have been extended, and that at the same time implement IDisposable, and
  2. a timer that raises an event that validates what of those objects are still alive or not. If they are not alive, then its meta-properties are processed to dispose those values that implements IDisposable, and whose AutoDispose property is set to True.

Note: This AutoDispose property is set as the third and optional parameter of the SetMetaProperty() method, which is True by default. If you don't want your value to be disposed automatically, even if it implements IDisposable, then set this AutoDispose property to false.

It is an optional mechanism. It only starts to keep track when you use the StartCollector() method of MetaPropertyExtender. It accepts an optional parameter that lets you tweak the interval (in milliseconds) at which the event is raised to accommodate to your particular needs, performance restrictions, or your particular taste. You can call this method several times, with the effect of modifying the interval of the timer.

You can also stop the collector with the StopCollector() method – note that in this case any object that may remain in the internal list of tracked objects is not processed... but is not removed from this list. So, if you start the collector again, they will be present and ready to be processed.

Yes, I agree, it is not as deterministic as calling Dispose() on those values stored in the meta-properties (*), but if you decide not to check manually for the existence of meta-properties on your host instances, or maybe you cannot because you cannot alter them, this is a fallback mechanism that may help.

(*) Firstly, there is the delay at which the timer ticks. But also note that even if you call Dispose() on an object, for whatever reason it appears as alive for a long time before in the weak reference it is identified as disposed (alive being false). Seems to be related to the cadence at which the GC runs, but I cannot assure it completely. So, as it is now, there is no way to be immediately informed that Dispose() has been called in the host object. At least, no way that I know (or wanted to implement).

History

  • [v1, May 2012]: Initial version
  • [v1, May 2012]: Version 2 - Including an optional collector to dispose values stored in the meta-properties, if needed

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here