Click here to Skip to main content
Click here to Skip to main content

C# Easy Extension Properties

, 30 May 2012
Rate this:
Please Sign up or sign in to vote.
(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# does not provide support for "Extension Properties". In this article I introduce a solution to provide such capability, based in a mechanism that is already present in the runtime and used extensively by it: the TypeDescriptor class.

Some explanations

Since version 4.0, C# provides support for "Extension Methods". They provide the programmer with the illusion that the types can be extended, even if the source code is not available or if the type is sealed... and with no need to create derived classes. What’s more, those extensions can be applied to instances of the types extended at run-time, regardless who has created those instances. In reality this is just an illusion supported by the compiler, but it works pretty well.

One drawback is that this mechanism does not allow to "injecting" any state into those instances – basically this is what is needed to create the illusion you have added some properties to any given object at run time. 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.

Until now those situations were solved creating some short of wrapping around the object you want to extend, with a class that inherits from the original type of the object, using proxy factories (á la AOP/IoC), mixins, reflection and emit, dynamics and the like. Those are extremely powerful alternatives, and the Internet is plenty of information that permits you to build your own library, or use any of the available ones. But also those typically complex and heavy weighted, and they tend to have a substantial impact on performance.

Also, when you have to deal with an unknown number of different types, potentially a big one, then there would be a problem trying to generate those wrappers using reflection and emit at run time. Performance will suffer, to say the least, not to mention that scalability will be also compromised.

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 permits to attach an attribute to any object 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. As you can easily find by yourself, this mechanism is used extensively by the runtime to store attributes each instance is carrying with it. This mechanism has been around for ages, it is solid, and reliable.

So, using this mechanism, we will use a custom attribute to store the information we need. We are going to define the 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. These 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 those contents in an easy way.

Even if this mechanism solves the need to store any arbitrary information on any arbitrary instance, it is not able to mimic completely the syntax we use with the standard or 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 not 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 methos 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 ListMetaProperties(): 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, is just a matter of manipulate 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 it 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 achive something similar. Basically, it consist in (a) a list of WeakReferences that tracks those objects that have been extended, and that at the same time implement IDisposable, and (b) 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

  • [May 2012]: Initial version.
  • [May 2012]: Version 2: Including an optional collector to dispose values stored in the meta-properties, if needed.
  • [May 2012]: Version 2.5: Performance improvements and refactoring of the internal classes.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Moises Barba

Spain Spain
Moises Barba has worked as CIO and CTO for some start-ups, and as Consulting Director for some major multinational IT companies. Solving complex puzzles and getting out of them business value has ever been among his main interests - that's why he has spent his latest 20 years trying to combine his degree in Theoretical Physics and his System Engineer MCSE with his MBA... and he is still trying to figure out how all these things can fit together. Even if flying a lot across many countries, along with the long working days that are customary in IT management and Consultancy, he can say that, after all, he lives in Spain (at least the weekends).
Follow on   Twitter   LinkedIn

Comments and Discussions

 
GeneralMy vote of 5 Pinmembersravani.v29-May-12 0:57 
GeneralRe: My vote of 5 PinmemberMoises Barba29-May-12 2:46 
GeneralMy vote of 5 Pinmembermicky_bird8628-May-12 15:26 
GeneralRe: My vote of 5 PinmemberMoises Barba29-May-12 2:46 
GeneralMy vote of 5 Pinmemberburndavid9327-May-12 13:12 
GeneralRe: My vote of 5 PinmemberMoises Barba29-May-12 2:45 
QuestionNice idea, but the implementation could be better. PinmvpPaulo Zemek24-May-12 7:40 
AnswerRe: Nice idea, but the implementation could be better. PinmemberMoises Barba24-May-12 7:47 
GeneralRe: Nice idea, but the implementation could be better. PinprotectorAspDotNetDev24-May-12 13:11 
GeneralRe: Nice idea, but the implementation could be better. PinmvpPaulo Zemek24-May-12 13:36 
GeneralRe: Nice idea, but the implementation could be better. PinprotectorAspDotNetDev24-May-12 14:14 
GeneralRe: Nice idea, but the implementation could be better. PinmemberMoises Barba24-May-12 22:14 
GeneralRe: Nice idea, but the implementation could be better. PinprotectorAspDotNetDev25-May-12 4:39 
QuestionProblem to download version2 ? Pinmemberchch23-May-12 20:59 
AnswerRe: Problem to download version2 ? PinmemberMoises Barba23-May-12 21:15 
QuestionActually there is already something for this in the .NET framework PinmvpSacha Barber23-May-12 0:42 
AnswerRe: Actually there is already something for this in the .NET framework PinmemberPaw Jershauge23-May-12 1:25 
GeneralRe: Actually there is already something for this in the .NET framework PinmvpSacha Barber23-May-12 5:04 
GeneralRe: Actually there is already something for this in the .NET framework PinmemberMoises Barba30-May-12 10:45 
GeneralMy vote of 5 Pinmemberjfriedman22-May-12 12:18 
GeneralRe: My vote of 5 PinmemberMoises Barba22-May-12 22:23 
GeneralMy vote of 5 PinmemberPaw Jershauge22-May-12 11:27 
GeneralRe: My vote of 5 PinmemberMoises Barba22-May-12 11:49 
QuestionUse case scenarios PinmemberGalatei21-May-12 10:12 
AnswerRe: Use case scenarios PinmemberMoises Barba21-May-12 23:48 
AnswerRe: Use case scenarios PinmemberThorsten Bruning22-May-12 8:38 
QuestionNice approach PinmemberThorsten Bruning18-May-12 23:12 
AnswerRe: Nice approach Pinmembermbarbac19-May-12 0:45 
GeneralRe: Nice approach PinmemberThorsten Bruning19-May-12 0:57 
GeneralRe: Nice approach PinmemberMoises Barba22-May-12 5:15 
GeneralRe: Nice approach PinmemberThorsten Bruning22-May-12 8:20 
GeneralRe: Nice approach PinmemberMoises Barba22-May-12 10:26 
GeneralMy vote of 5 PinmemberEd Nutting18-May-12 22:21 
GeneralRe: My vote of 5 Pinmembermbarbac18-May-12 23:22 
QuestionActually... Pinmemberlewax0018-May-12 5:12 
AnswerRe: Actually... Pinmembermbarbac18-May-12 7:48 
GeneralRe: Actually... Pinmemberlewax0018-May-12 8:54 
GeneralRe: Actually... PinmemberZac Greve18-May-12 9:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140721.1 | Last Updated 30 May 2012
Article Copyright 2012 by Moises Barba
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid