Click here to Skip to main content
15,867,141 members
Articles / Programming Languages / C# 4.0

Universal Type Extender

Rate me:
Please Sign up or sign in to vote.
4.83/5 (30 votes)
5 Jun 2013Ms-PL11 min read 72K   569   62   41
Emulate extension properties by extending any reference type with any other type.

Table of Contents

Introduction

We all know that there are no extension properties. Extension methods, yes - but no extension properties. If this has already been a problem to you, this article might be a solution.

Of course, you should prefer the "clean" way and extend your types if you need new properties. But that is not always possible - there are sealed classes within the .NET Framework or within third party assemblies. Or it just seems to be an overhead to extend a type for using that property in only one case. To make it short, I will not discuss the pros and cons about extension properties - you can use them just as you like.

But I will give you an example: I had to implement a module where all kinds of types came in - maybe once, maybe twice, do not know how often. But I had to recognize an instance the second time it came in. So I wanted to add a "ticket" to any instance. Something as following would be nice:

C#
theIncomingReference.TicketID = new Guid();
theIncomingReference.TicketTime = DateTime.Now;

So I can do something like this:

C#
if(theIncomingReference.TicketID != null) {return;}

Of course, without theIncomingReference having implemented "TicketID" or "TicketTime" as a property!

Just in case I could benefit from others work, I searched the internet for extension properties. No direct support was found. I came across Mixin. But that was not what I was looking for. You will have to use interfaces and factory methods - not as generic as I wanted it to be. There was really no solution for me. So I did it my way (good old Franky) and came up with the UniversalTypeExtender.

At the end, my solution looks like this:

C#
theIncomingReference.Extended().Dynamically().TicketID = new Guid();
theIncomingReference.Extended().Dynamically().TicketTime = DateTime.Now;

Or by using a concrete type as an extension:

C#
theIncomingReference.Extended().With<Ticket>().TicketID = new Guid();
theIncomingReference.Extended().With<Ticket>().TicketTime = DateTime.Now;

Of course, you can read the properties, later on:

C#
Guid ticketID = (Guid) theIncomingReference.Extended().Dynamically().TicketID;

Or more type safe:

C#
Guid ticketID = theIncomingReference.Extended().With<Ticket>().TicketID;

This is it (good old Michael) - working on all reference types without setting up anything - no interfaces, no inheritance, no factory!

Interested in how it works?

Overview

Again, there is no support for extension properties out of the box. So we will have to emulate this. This is done by "binding" a reference of a type (e.g., Ticket) to the extended object (e.g., theIncomingReference). This "binding" is stored and it is used again on every request on the extended object. So, actually you work with properties of the "binding" object instead with properties of the extended object. The trick is that you always get the same "binding" object instance for the same extended object. Sounds easy? Yes, but of course there are a few things to take care of - like garbage collection. We will have a look at this, soon. But first, a class diagram showing the components of the UniversalTypeExtender:

ClassDiagram

You can see the "binding" object in the class diagram. It is called RepositoryItem and comes as a nested class within its related ExtensionRepository. A RepositoryItem holds a reference to the extended object and the extensions of this object. The ExtensionRepository keeps track of these RepositoryItems. The UniversalTypeExtender itself provides only one extension method - Extended<T>(). This method returns an Extender for the object to extend. The Extender in turn manages the different ways of extending an object - for now, Dynamically() and With<TE>(). Dynamically() returns a real dynamic object (DynamicPropertyExtension) - that is why you can call any property without setting up anything else. With<TE>() returns an instance of the specified type - that gives you type safe coding.

So, that was a short overview. Let's have a deeper look at each of these components.

UniversalTypeExtender

As already mentioned, all you have to do in order to extend an object is to call the extension method Extended<T>(). That was really obvious because extension methods are the only way to add our own functionality to any type without touching the code. By using the generic type T, we can control that extensions only work on reference types - that means classes. It would be problematical on value types because they are mostly given by value. But the concept behind the extender needs references as you can see later on. Extended<T>() returns an Extender which gets a reference to the extended object. So the code looks really simple:

C#
public static Extender<T> Extended<T>(this T extendee) where T : class {
    Contract.Requires<ArgumentNullException>(extendee != null);
    return new Extender<T>(extendee);
}

Also, the UniversalTypeExtender holds an instance of an ExtensionRepository by implementing the singleton pattern:

C#
private static volatile ExtensionRepository mRepositoryShouldOnlyBeCalledFromProperty;
private static readonly object SyncRoot = new object();

private static ExtensionRepository Repository {
    get {
        if (mRepositoryShouldOnlyBeCalledFromProperty == null) {
            lock (SyncRoot) {
                if (mRepositoryShouldOnlyBeCalledFromProperty == null) {
                    mRepositoryShouldOnlyBeCalledFromProperty = 
                               new ExtensionRepository();
                }
            }
        }
        return mRepositoryShouldOnlyBeCalledFromProperty;
    }
}

For further information about this implementation, look here or here. It just ensures that there is only one instance of an ExtensionRepository created.

Extender

Again, a very unspectacular piece of code. An Extender just holds a reference to the object to extend and provides a set of methods for the different ways of extending this object:

C#
public class Extender<T> where T:class {
    private readonly T mExtendee;

    internal Extender(T extendee) {
        Contract.Requires<ArgumentNullException>(extendee != null);
        mExtendee = extendee;
    }
    
    public dynamic Dynamically() {
        return With<DynamicPropertyExtension>();
    }

    public TE With<TE>() where TE:class, new() {
        return Repository.GetExtension<TE>(mExtendee);
    }
}

As you can see, the real work is delegated to the repository. For that, the With<TE> forces TE to provide a parameterless constructor because the repository has to create a new instance of TE on its first request. Also, you can see that Dynamically() just calls With<TE> with a predefined type - DynamicPropertyExtension, at which we will come back later on.

You might wonder why I introduced the Extender instead of just implementing proper extension methods on the UniversalTypeExtender. I wanted the extension method Extended() to be limited to reference types. So I had to define Extended<T>() with T being limited to classes. You do not have to think about T - just type Extended(). But if I had to define TE (from With<TE>()), too - like Extended<T, TE>() - you would have to specify T on any call. I did not like that, that's all TheUniversalTypeExtender/smiley_wink.gif Consider the Extender to be part of a tiny fluent interface.

But let's continue with the ExtensionRepository...

ExtensionRepository

The repository holds a dictionary of RepositoryItems. As you have seen above, the Extender calls GetExtension<T>():

C#
public T GetExtension<T>(object forInstance) where T : class, new() {
    return Get(forInstance).GetExtension<T>();
}

which in turn calls Get<T>():

C#
public RepositoryItem Get<T>(T forInstance) where T:class {
    lock (mSyncRoot) {
        return GetOrNew(forInstance);
    }
}

This method locks the repository in order to ensure that only one thread has access to the inner item dictionary which is used in the called GetOrNew method:

C#
private readonly Dictionary<RepositoryItemTargetKey, RepositoryItem> mItems = new 

Dictionary<RepositoryItemTargetKey,RepositoryItem>();

private RepositoryItem GetOrNew(object forInstance) {
    RepositoryItem item;
    if (mItems.TryGetValue(new RepositoryItemTargetKey(forInstance), out item)) {
        return item;
    }
    return NewItem(forInstance);
}

private RepositoryItem NewItem(object forInstance) {
    RepositoryItem item = new RepositoryItem(forInstance);
    mItems.Add(item.Key, item);
    StartGCTimer();
    return item;
}

Locking is used because the repository has to make sure that a call gets a "safe" view of the item dictionary. Imagine, a call would create a new item because the requested item is not there, yet. And at the same time - before the new item is added to the dictionary - another call would do the same. That would end up in two extension items for the same object! But that would bypass our concept - so we lock.

GetOrNew looks for an item which belongs to the requested instance. If no item was found, it is created. Otherwise it is returned.

Garbage Collection

Before we will inspect the RepositoryItem, we will have a look at garbage collection. This is the .NET way of freeing the memory of unused objects. If you have not heard about it before, you can get more information here. The important thing to know is that it removes "unused" objects. That means objects which are not referred any more. Now, have a look at our repository again: all created items are stored in the inner item dictionary. That means, this dictionary is getting bigger and bigger. Because all these items are referred in one way - at least from the dictionary itself - there is no automatic garbage collection from the framework. So we will have to take care of this on our own. And of course, we do! If you dig into the ExtensionRepository's code, you will notice that there is a timer which looks after garbage from time to time:

C#
public void CollectGarbage() {
    lock (mSyncRoot) {
        List<KeyValuePair<RepositoryItemTargetKey,
           RepositoryItem>< itemsToRemove = mItems.Where(item => 

item.Value.CouldBeCollectedByGC).ToList();
        foreach (var item in itemsToRemove) {
            mItems.Remove(item.Key);    
        }
        if (mItems.Count == 0) {
            StopGCTimer();
        }
    }
}

This method asks every item if it could be collected (how the item itself knows about this is described soon) and removes it, if so. Again, watch out for locking! You can figure out when the timer starts and stops on your own. You can define the interval for garbage collection by setting the CollectGarbageIntervalInMilliseconds property of the UniversalTypeExtender. Its default is 15.000 milliseconds.

Garbage collection seems to be just a tiny part of this article, but it really is an important part of the UniversalTypeExtender.

RepositoryItem

Now let's inspect the RepositoryItem. It holds a reference to an extended object and references to all extensions requested for this object. As you can see, the reference to the extended object is stored as a WeakReference:

C#
private readonly WeakReference mTargetReference;

public RepositoryItem(object target) {
    Contract.Requires<argumentnullexception>(target != null);
    mTargetReference = new WeakReference(target);
}

If it would be stored as a "normal" field - that means as a strong reference - it would never be collected by the garbage collection, as mentioned above. But WeakReferences give you the ability to hold a reference by allowing it to be collected if there is no other strong reference to it anymore. So, you can check if a reference is still alive:

C#
public bool CouldBeCollectedByGC {
    get { return !mTargetReference.IsAlive; }
}

Fine! If an extension is requested, there are nearly the same things to do like described for the repository: locking, searching within the list, creating an instance, and so on:

C#
private readonly object mSyncRoot = new object();
private readonly List<object> mExtensions = new List<object>();

public T GetExtension<T>() where T : class, new() {
    lock (mSyncRoot) {
        return GetExtensionOrNew<T>();
    }
}

private T GetExtensionOrNew<T>() where T : class, new() {
    object extension;
    for (int i = 0; i < mExtensions.Count; i++) {
        extension = mExtensions[i];
        if (extension is T) {
            return (T) extension;
        }
    }
    return NewExtension<T>();
}

private T NewExtension<T>() where T : class, new() {
    T newExtension = new T();
    mExtensions.Insert(0, newExtension);
    return newExtension;
}

RepositoryItemTargetKey

The RepositoryItemTargetKey is used as key for the inner dictionary of the repository. It is necessary for generating a proper key for a weak reference, which is a fundamental thing for the UniversalTypeExtender. In my first version, there was a simple list holding the items. Then, Mike Marynowski pointed me to the dictionary and gave me an idea of how to use it for weak references. That gave my implementation an enormous performance boost. Thanks, Mike! To make it short, the RepositoryItemTargetKey delegates GetHashCode and the Equals method to the weak reference. For more details, I recommend to look into the code and to read Mike's comments below this article.

DynamicPropertyExtension

As shown at the beginning, you can extend an object by calling theIncomingReference.Extended().Dynamically(). Then, you can type any property name as you want and set it to any value you like. Is it a miracle (good old Freddy)? If you know the DynamicObject from .NET 4, you already know the answer, which - of course - is: no! The DynamicObject lets you implement dynamic property access. There are special methods you can override, building the getter and setter of your properties. In this case, the values are stored within a private list - of course with their property names:

C#
private readonly Dictionary<string, object> mProperties = new Dictionary<string, object>();

public override bool TryGetMember(GetMemberBinder binder, out object result) {
    return TryGetPropertyValue(binder.Name, out result);
}

public override bool TrySetMember(SetMemberBinder binder, object value) {
    SetPropertyValue(binder.Name, value);
    return true;
}

private bool TryGetPropertyValue(string propertyName, out object result) {
    return mProperties.TryGetValue(propertyName, out result);
}

private void SetPropertyValue(string propertyName, object value) {
    mProperties[propertyName] = value;
}

You should be aware of just working with an object type here - not very type safe, indeed! There is support for using an index as getter and setter, too. But this is nearly the same. So, just look into the source, if you like. That's it! You made it through the whole story!

Using the code

The code makes use of Code Contracts and FluentAssertions for testing. So you will have to install them - both for free - in order to compile and test the source code. But the final DLL can be found in the bin folder - ready to use. Just reference it and distribute it with your final binaries. The UniversalTypeExtender comes as an extension method. You will have to reference the TB.ComponentModel namespace in order to use it.

Limitations

  • As pointed out, it just works for reference types.
  • Extensions you want to use by the With<TE>() method will have to implement a parameterless default constructor.
  • As you have seen, the objects are not touched in any way. The extensions are just stored within a static list. That means that, e.g., serialization will not work. So, putting an extended object into the ViewState of an ASPX page - in hope of getting the extensions back on a postback - will not work, of course. Whereas storing an object in the Session should do fine.

Performance

Just for interest, I did a little setup for testing the performance. This test was done on a Core Duo 2.67 GHz with 3 GB RAM. During the test, 10,000 objects were created and each of them was extended. Then these 10,000 objects were looped again and the extension was read for each. The garbage collection of the UniversalTypeExtender was called during the test. The average duration of the test was about 0.8 seconds - for 20,000 calls total! Just to compare: my first implementation - working with a list instead of a dictionary - took 8.5 seconds for the same test.

Conclusion

If something is not supported by the framework, you will have to do it on your own TheUniversalTypeExtender/smiley_wink.gif.

Points of interest

While using WeakReferences, I had to handle them in my unit tests. That means, I had to control that they were collected by garbage collection so that I could verify some actions within my code. Normally, you could not - and you should not! - control the time of collecting by yourself. You can trust the runtime - it knows best when to do this. But for testing, you will have to control it. Therefore, you can call GC.Collect() as you can see in some of my tests.

Sacha Barbar pointed me to the ConditionalWeakTable class which is a kind of dictionary and implements the functionality I needed. This means, storing objects as weak references and garbage collecting of these. This seems to be the best solution and maybe I will change it in future. Thanks to Sacha for this great information.

History 

  • 5 Jun 2013: Typo fixed 
  • 4 Nov 2011: Mentioned the ConditionalWeakTable class in the Points of interest section.
  • 23 Oct 2011: Uses a dictionary instead of a list; article and source updated.
  • 18 Oct 2011: Performance section added.
  • 10 Oct 2011: Bug fixing, source updated.
  • 3 Oct 2011: First version.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


Written By
Software Developer
Germany Germany
--------------------------------
Thank you for voting
Best "Everything Else" Article
of September 2014

Comments and Discussions

 
GeneralMy vote of 5 Pin
Daniel Gidman4-Nov-11 4:27
professionalDaniel Gidman4-Nov-11 4:27 
GeneralRe: My vote of 5 Pin
Thorsten Bruning4-Nov-11 5:57
Thorsten Bruning4-Nov-11 5:57 
GeneralMy vote of 5 Pin
Philip Liebscher2-Nov-11 10:29
Philip Liebscher2-Nov-11 10:29 
GeneralRe: My vote of 5 Pin
Thorsten Bruning3-Nov-11 2:32
Thorsten Bruning3-Nov-11 2:32 
Nice, thank you very much, Philip.
10 Home
20 Sweet
30 goto 10

GeneralThere is an existing framework solution that does this already I think Pin
Sacha Barber24-Oct-11 3:02
Sacha Barber24-Oct-11 3:02 
GeneralRe: There is an existing framework solution that does this already I think Pin
Thorsten Bruning24-Oct-11 4:49
Thorsten Bruning24-Oct-11 4:49 
GeneralRe: There is an existing framework solution that does this already I think Pin
Sacha Barber24-Oct-11 20:14
Sacha Barber24-Oct-11 20:14 
GeneralRe: There is an existing framework solution that does this already I think Pin
Thorsten Bruning24-Oct-11 21:11
Thorsten Bruning24-Oct-11 21:11 
GeneralRe: There is an existing framework solution that does this already I think Pin
Sacha Barber25-Oct-11 0:18
Sacha Barber25-Oct-11 0:18 
GeneralRe: There is an existing framework solution that does this already I think Pin
Thorsten Bruning3-Nov-11 21:53
Thorsten Bruning3-Nov-11 21:53 
GeneralMy vote of 5 Pin
Nicolas Dorier23-Oct-11 10:13
professionalNicolas Dorier23-Oct-11 10:13 
GeneralRe: My vote of 5 Pin
Thorsten Bruning23-Oct-11 20:06
Thorsten Bruning23-Oct-11 20:06 
QuestionPerformance Improvement Pin
Mike Marynowski17-Oct-11 22:10
professionalMike Marynowski17-Oct-11 22:10 
AnswerRe: Performance Improvement Pin
Thorsten Bruning17-Oct-11 23:29
Thorsten Bruning17-Oct-11 23:29 
GeneralRe: Performance Improvement Pin
Mike Marynowski17-Oct-11 23:55
professionalMike Marynowski17-Oct-11 23:55 
GeneralRe: Performance Improvement Pin
Thorsten Bruning18-Oct-11 0:56
Thorsten Bruning18-Oct-11 0:56 
GeneralRe: Performance Improvement Pin
Mike Marynowski18-Oct-11 2:35
professionalMike Marynowski18-Oct-11 2:35 
GeneralRe: Performance Improvement Pin
Thorsten Bruning18-Oct-11 5:51
Thorsten Bruning18-Oct-11 5:51 
GeneralRe: Performance Improvement Pin
Thorsten Bruning18-Oct-11 6:21
Thorsten Bruning18-Oct-11 6:21 
GeneralRe: Performance Improvement Pin
Mike Marynowski18-Oct-11 6:51
professionalMike Marynowski18-Oct-11 6:51 
GeneralRe: Performance Improvement Pin
Thorsten Bruning18-Oct-11 7:52
Thorsten Bruning18-Oct-11 7:52 
GeneralRe: Performance Improvement Pin
Mike Marynowski18-Oct-11 9:46
professionalMike Marynowski18-Oct-11 9:46 
GeneralRe: Performance Improvement Pin
Thorsten Bruning23-Oct-11 9:33
Thorsten Bruning23-Oct-11 9:33 
GeneralRe: Performance Improvement Pin
Mike Marynowski23-Oct-11 11:22
professionalMike Marynowski23-Oct-11 11:22 
GeneralRe: Performance Improvement Pin
Thorsten Bruning23-Oct-11 20:04
Thorsten Bruning23-Oct-11 20:04 

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

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