.NET Extension Methods

Dear loyal readers (all 3 of you),

Thank you for your patience. This is my first blog since the birth of my first child. The balance of work/life tipped in the direction of life for a while, and I’ve been playing catch up ever since. Sorry to keep you waiting for me to bestow my wisdom upon
you. So without further adieu, let’s get started.

Today we’re going to introduce .NET Extension Methods, briefly explain what they are and how they work, and discuss the advantages, potential pitfalls and best practices for this feature.

What are extension methods?

It’s not easy to describe, but you could say extension methods are functions that are written outside of the original class file. They attach themselves to the class during compilation so you can add your own custom functions or override built in functions of framework classes that were previously closed off to alteration. If you are coming from a dynamic language background, this is similar to prototype methods.

Here an example in VB.NET. It returns the fiscal year of a datetime object. This is a domain specific method. In the government, the fiscal year is shifted by 3 months, beginning on October 1st and ending September 30th. Other businesses might have a different fiscal year. Note: DateTime.AddMonths() returns a new DateTime without altering the original.

Example of Extension Method

 vb.net |  copy code |? 
01
02
 
03
Imports System.Runtime.CompilerServices
04
 
05
Public Module DateTimeExtensions
06
 
07
   <Extension()>
08
   Public Function FiscalYear(ByVal dateTime As DateTime) As Integer   
09
      Return dateTime.AddMonths(3).Year
10
   End Function
11
 
12
End Module
13

In an extension method, the first parameter is the class being extended. The method is invoked like any other function of that class. So you would call this function like this:

 vb.net |  copy code |? 
1
2
Dim dt as New DateTime('2011-10-15')
3
Dim fy = dt.FiscalYear() 'returns 2012 
4

Alternates

Before discussing deeper methodology, lets look at a few examples of alternate patterns and briefly list their advantages and disadvantages.

Inherited Class

A custom class that extends a framework class and exposes extended methods:

 vb.net |  copy code |? 
01
02
Public Class GovDateTime Extends DateTime
03
 
04
   Public Function FiscalYear() As Integer
05
      Return AddMonths(3).Year
06
   End Function
07
 
08
End Module
09
 
10
Dim dt as New DateTime('2011-10-15')
11
Dim gdt As GovDateTime = Ctype(dt, GovDateTime)
12
Dim fy = gdt.FiscalYear() 'returns 2012 
13
 
14

Advantages:
Clearly a domain specific class.

Disadvantages:
Not discoverable. Requires casting to work with existing DateTime objects. Will throw an exception if object is null.

Wrapper Class

A custom class that contains a framework class and exposes extended methods:

 vb.net |  copy code |? 
01
02
Public Class GovDateTime
03
 
04
   Private _DateTime As DateTime
05
 
06
   Public Sub New(Byval dt as DateTime)
07
       _DateTime = dt
08
   End Sub
09
 
10
   Public Function FiscalYear() As Integer
11
      Return _DateTime.AddMonths(3).Year
12
   End Function
13
 
14
End Module
15
 
16
Dim dt as New DateTime('2011-10-15')
17
Dim gdt As New GovDateTime(dt)
18
Dim fy = gdt.FiscalYear() 'returns 2012 
19
 
20

Advantages:
Clearly a domain specific class. Can limit access to just the functions you want. Can safely handle nulls.

Disadvantages:
Not discoverable. More difficult to refactor since you are no longer working directly with the framework class.

Helper Class

A static (or shared) class that applies the extended logic to other classes:

 vb.net |  copy code |? 
01
02
Public Class DateTimeHelper
03
 
04
   Public Shared Function FiscalYear(ByVal dateTime As DateTime) As Integer
05
      Return dateTime.AddMonths(3).Year
06
   End Function
07
 
08
End Module
09
 
10
Dim dt as New DateTime('2011-10-15')
11
Dim fy = DateTimeHelper.FiscalYear(dt) 'returns 2012 
12

Advantages:
Clearly a domain specific class. No casting required.

Disadvantages:
Not discoverable. Reverses the natural syntax of noun then verb. (think dog.IsRunning vs. DogHelper.IsRunning(dog))

The Inline Approach

Just smack the logic inline wherever you need it. Probably the most popular method for microfunctions:

 vb.net |  copy code |? 
1
2
Dim dt as New DateTime('2011-10-15')
3
Dim fy = dt.AddMonths(3).Year
4

Advantages:
No discoverability required – nothing to discover.

Disadvantages:
Logic is decentralized and duplicated. Code becomes more complex to read and harder to maintain if logic changes. Pretty much violates every rule of object oriented programming.

Advantages of Extension Methods

Discoverability

If you didn’t already know, discoverability refers to the ability to find code while you’re writing code. This method shows up with intellisense when working with basic framework classes. In order to use a custom class, a developer would first have to know (and remember) that a custom class exists and know where to find it. With larger project it often becomes faster to duplicate logic than to search for it in your code base.

Readability

When compared to the other examples, the implementation of the extension method reads the most naturally. This is possibly even more pronounced when overriding built-in methods like ToString(), which often returns useless values like ‘[Object Classname]’.

Safety with null objects

If you call a method on a null object, you get a null object reference exception. This is probably one of the most common exceptions in code, and can be difficult to track down because it’s so generic.

So dt.Year will throw an exception if dt is null.

But dt.FiscalYear() actually calls FiscalYear(dt) behind the scenes. So, if we put a null check in our function, we can call this function with a safe default response in case of nulls.

 vb.net |  copy code |? 
1
2
   <Extension()>;
3
   Public Function FiscalYear(ByVal dateTime As DateTime) As Integer
4
      'check for null and return a "safe" value, the current fiscal year for example.
5
      If dateTime is Nothing then return DateTime.Now.AddMonths(3).Year
6
      Return dateTime.AddMonths(3).Year
7
   End Function
8

This function is probably not the best example, but the point is that you have control over how to handle nulls. You could just not check for nulls if you want the exception, or you can return a “safe” value.

Easier separation in n-Tier applications (huh?)

I could write a whole post on this point alone, but suffice it to say, keeping pure separation between your presentation, business and data layers is nearly impossible or requires some sacrifice in organization. A common example would be a function that outputs html for a web app. You don’t want to include that in a business or data class. But you can write an extension in your web layer to ensure good layer separation.

Lightweight

Oftentimes you only need a single extension method. Adding an extra class, a new file, and custom code is a lot more burdensome than making one method.

Potential Pitfalls

I elect not to call this section “disadvantages” because the potential harm comes more from bad practices and programmer misunderstanding rather than the feature itself.

Confusion with Original Framework

We had an extension method html.Image(filename) which added (without duplicating) the path to the images folder in our app. Not a bad method. Keeps markup concise and centralizes a folder path so it can easily change. I realized there was a problem when a new developer claimed to now know how to include images in MVC. They would be very confused if they copied that code to a different project!

While you could put the responsibility on new developers to learn the feature, we live in the real world where people copy and paste code blindly. Hiding custom code within what appears to be framework can not only cause confusion, especially if extension methods behave differently than framework methods.

Our workaround for that project was to make extensions that returned a domain specific wrapper class for the extension methods. So the code became html.OurProject.Image(filename). Still discoverable but more explicitly a domain object.

Higher Potential for Class Collision

If two extension methods have the same name, you will have a collision issue. This can happen as coders carry around their bag of tricks to different project and tend to use common names and add extension methods to primative classes. An easy workaround is to place a namespace around your extensions, but when you do that you lose intellisense discoverability unless that namespace is included.

Promotes bad habits due to safety with nulls

It’s good practice to check if an object is null before accessing any method. “Swallowing” exceptions is rarely the right thing to do. What can seem like it’s making your code safer may actually be creating hidden bugs. You have to be extremely cautious when checking for null. I prefer function names like ValueOrDefault() or ValueOrNothing() to show that I’m handling nulls internally.

Overuse can cause “Language Spam”

Coders have a tendency to write just enough code to get done what they need at the time without much consideration to class design. Framework classes, by comparison, have been meticulously thought out, reviewed, and fine-tuned over years. It’s very likely the framework already provides a method that could be used in place of the one you’re writing, possibly several. The sheer vastness of the framework can make things hard to find.

Adding hundreds of extension methods to a single class is probably not a good idea. But patterns are repeated and code is copied, so it’s easy for this to happen. To avoid this, ask if your extension method needs to be easily discovered. Also consider what will happen if it’s accidentally discovered. Will it cause confusion?

Altering Framework Conventions

Significantly altering a function signature by using a new convention makes your method more difficult to work with. Try to make your methods mirror the framework as closely as possible. Generic names should be reserved for functions that work seamlessly with the framework. Use Domain specific names for domain specific functions. Even though my FiscalYear implementation was specific to the government, the concept of a FiscalYear is general, so if the code was imported into a business with a different fiscal year, you would just have to change how the method works rather than the entire code base (as in the inline approach).

Breaking Class Design

Also, classes modeled after real-world objects lose their meaning if you add methods that break the metaphor. For instance Employee.ToByteArray() doesn’t make any sense. If you’re not careful, you can add so many specific functions that you’ve virtually changed the definition of the class.

Summary

In the long standing debate between static and dynamic languages, most experts will agree that the “perfect” language would lie somewhere in the middle, combining the flexibility of dynamic languages with the integrity of static. In the static realm of C# and VB, language extensions take a step toward the middle, allowing access to modify previously immutable classes. If implemented judiciously and well understood by all members of a development team, it can be a very powerful tool in your bag of tricks. Consideration for novice developers should be make, but that doesn’t mean avoiding advanced techniques. You just need to use them safely, hide away as much as possible and educate team members on your practices. Your project doesn’t need to be a training ground for novices, but it shouldn’t be a proving ground either.

This entry was posted in asp.net. Bookmark the permalink.

4 Responses to .NET Extension Methods

  1. John Pencola says:

    Nicely articulated! I read Microsoft’s description of this feature and based on how these currently work you could add another warning on a potential pitfall. It goes something like this in a very silly example:

    Lets say you create an extension method on the String class called “ellipsify” with an argument that specifies how many characters are allowed before the string is truncated and appended with four dots – “….”

    A year from now Microsoft adds an instance method on the String class and it happens to have the same signature; ellipsify – but instead they append three dots to the end of the returned string. Behind the scenes now your code defaults to using the String class’s ellipsify instance method instead of your extension — with no indication through compilation error or warning.
    Maybe a way to mitigate such a problem would be to check if the object has the instance method before extending it with yours?

    -JP

  2. mindstorm says:

    @jpencola Interesting point. I had not read that and I find it surprising. Using the same method name in an extension should throw an error that you must have an Overrides or Overloads declaration, depending on whether the signature matches or not. Seems like a very easy problem for the compiler to solve and one that Microsoft will probably fix. Extension methods are relatively new so they will surely evolve as Microsoft receives community feedback. It’s very likely that common custom extension methods will find their way into downloadable libraries if not the actual framework itself.

  3. mindstorm says:

    The scenario of a duplicate named method being added to the framework is actually quite likely, especially considering the fact that programmers coming from different languages may want to write methods that simulate their previous environment. Funny you should point out elipsify as an example. My codebase has an actual extension method called “Truncate” which does pretty much the same thing.:

     vb.net |  copy code |? 
    1
    
    
    2
    <Extension()>
    3
     Public Function Truncate(ByVal str As String, ByVal length As Integer, Optional ByVal appendTextIfTruncate As String = "...") As String
    4
          If str.Length <= length Then Return str
    5
          Return str.Substring(0, length) & appendTextIfTruncate
    6
    End Function
    7
    
    

  4. John Pencola says:

    Nice! Yea, I’m interested too. I can’t imagine that its an oversight, so I’m curious as to why they didn’t just build a warning feature or some something similar into the compiler, maybe there is more to it… Keep us posted!

    -JP

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>