Wednesday, July 23, 2008

VS Add-in: Extending Sealed Classes (without Extension methods)

If you are ever in the situation when you have a .NET Framework class with most of the functionality you want, but not really doing everything to the extent you need, you have a few options:

  • You can inherit from the class and extend as you see fit

  • You can create a utility class to perform the extra functionality

  • You can use extension methods to add functionality to the class

  • You can create a new wrapper class which holds the class as an internal variable and create wrapper-methods for whatever methods you need from the class you are extending, and add any extra functionality you need.

Inheriting from the class is likely the best approach. Unfortunately, about 40% of the .NET 2.0 Framework classes are sealed, which of course means you can't inherit from them.

The second approach is becoming somewhat obsolete, as it can lead to a sort of functional programming which takes you away from proper object-oriented programming. It makes things just a bit harder to find and understand.

Number three, extension methods, is the new cool kid on the block. Add new methods just like magic to any class. The possibilities are enormous! The problem is that you can't do anything with the internals of the class, you can only add new isolated methods.

The last approach can be the way to go if extension methods won't do. This way you'll have complete control over what you want to be public and you can extend and improve on existing and new functionality. There are two problems with this approach. Number one is clarity and number two is identity. If you create an extension of the DataSet for instance, it never is a dataset. You can, in a way, get around the issue by inheriting from one of it's base classes or interfaces though (Using that as a common base).

Be sure only to use it when no other approach is better. The clarity/identity issues can be confusing for someone seeing the class at first. But I do think the wrapper/composition approach have some merit, especially considering the extreme amount of sealed classes that exist. I made it because I was annoyed to find that there was no support for doing something like that besides typing it in manually. And if you have a lot of methods to wrap, that means a whole lot of time typing nonsense methods, not to forget the comments you should add if you want it understandable. No way I'm doing that unless I'm really keen on wasting a lot of time.

That's why I've created a code-generation add-on for the last approach. It's a Visual Studio Add-in to help extend sealed classes.



I’ll begin with showing some results, and then I’ll get to how and why it works/makes sense/you should use it. In this example, I'd like to extend the StringBuilder class. To do that I need to write something like:



Once I press SPACE or TAB while at the end of this line, the add-in starts its work on using this StringBuilderExtender class to extend System.Text.StringBuilder. The result is this:




More examples here and here.


What happened is this:
  • Added an internal StringBuilder instance

  • Recreated all public constructors, methods, properties and fields, and made sure all of them use the internal StringBuilder instance.

  • Added any available comment from the StringBuilder class.

  • Changed returntypes from StringBuilder to StringBuilderExtender where necessary.

  • Added the Serializable-attribute, since StringBuilder is serializable.

  • Listed the interfaces StringBuilder implements in a comment next to the class, in case you'd like to implement the same.

  • Added the System.Data namespace to the using statements if it did not exist.


Why would you want to do this?

  • You really want to extend one of the sealed classes.

  • You need new functionality in a sealed class, and you either only have .NET 2.0, or extension methods just won’t do it.

  • You reuse existing functionality, making a quick browse of the code enough to understand the majority of what the extended class does. Compare that to reimplementing a larger part of the functionality of a sealed class.

Why would you not want to do this?

  • You are somewhat pretending to extend a class you cannot. This can get you into problems with equality and comparison. The commented interfaces do make it simple to extend any common interfaces though.

  • Potential performance hit. You do add some overhead, and internal performance tweaks through for instance Win32 code could have has less effect.

More on the internals

In terms of what you can visually do
  • You need to specify the keywords for the add-in on the class line. The input must always be in the form specified above:

    • public class StringBuilderExtender SealedClassExtender(System.Text.StringBuilder)

    • or in other words

    • [visibility] class ClassName SealedClassExtenderKeyword(TypeFullName)

  • You can use both SealedClassExtender and the short form scx/SCX as keywords.

  • The TypeFullName is, obviously, the full name of the type.

  • You can also extend a class by specifying the path to the assembly. The form is then

    • public class StringBuilderExtender scx(System.Text.StringBuilder, C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\mscorlib.dll)

    • or

    • [visibility] class ClassName SealedClassExtenderKeyword(TypeFullName,FullAssemblyPath)


Apart from the list above it does a few more things in the background
  • Adds a default constructor if one isn’t specified in the base class.

  • With any method, property or field that returns the base type (StringBuilder), it returns the extended type instead (StringBuilderExtender). A new private constructor with the base type is added if necessary to fulfill this.

  • Static classes does not get an internal instance but just point directly at the class.

  • Lots more fun things to fix every special case.


If you do not specify a path it will try to resolve where to load it from by itself. It will currently do this by first trying to load the type from memory, then specifically checking all assemblies in the current application domain, and then trying to resolve the name of the assembly from the types full name. It will first try to look in the current runtime directory, then in the executing assembly path.

The comments are loaded by using the XML-document corresponding to most framework dlls, as comments are not part of the information retrievable through reflection.

Be aware though, the add-on will rewrite the entire document, so only add using-, class- and potentially namespace-declarations, anything else will be overwritten.

It doesn’t handle everything though. Specifically it doesn’t do:
  • Generics

  • Events

  • Identity-handling (Trying to guess how you want the identity-part handled will probably just end up with a bad guess, you’ll have to do this however you want it. Methods for equals, GetHashCode etc. are thus not created.)

  • Limited attribute support.

  • It also currently loads all assemblies into the current app domain while running. This is just because of a time-constraint on my part, and I’ll fix that.


If you have gotten this far, send me a note if you see a use for it, just don't get it, or have any comments.

Usage

To use this, you first need to install it like any other add-in. Copy the files in the zip to the correct Addins folder, for instance \My Documents\Visual Studio XXXX\Addins. (If the Addins folder does not exist, just create it yourself) Once this is done, restart visual studio, and you can enable the add-in in the "Tools->Add-in Manager"-dialog.

Downloads

Download the 2005 version from here.
Download the 2008 version from here.