Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VB

Extend the .NET Library with Extension Methods

4.21/5 (20 votes)
1 Apr 2009CPOL7 min read 68.4K  
With extension methods, you can add new functionality to .NET objects.

Introduction

With the introduction of LINQ in .NET 3.0, Microsoft added the ability to extend object functionality directly, without having to sub-class. This article will cover the basics of how to write extension methods. The techniques demonstrated require either Visual Studio 2008 or latter, or an earlier version of Visual Studio with the .NET 3.0 Framework.

Extension Methods

Writing an extension method is pretty easy. In Visual Basic, you write a method within a module, tack on the <Extension()> attribute from the System.Runtime.CompilerServices namespace, and have the first parameter set to the type of object that will be extended. In C# you create a static class, add your method as a static member, and have the first parameter set to the type of object that will be extended, using the this modifier. Now when IntelliSense pops up on objects of that type, your extension method will appear with the "extension" icon.

ExtensionType.PNG

There are a number of limitations to be aware of. You can only write extension methods, not properties or events. Extension methods can overload existing methods, but not override them. And if your extension methods are in a different namespace from the calling code, you must use ;the Imports (VB) or using (C#) statement to bring your extensions into scope.

Extending Strings: The Basics

What got me started thinking about extensions was the String object: specifically, the lack of a case-insensitive Replace method. I am adding merge functionality to an in-house application, and will have a base document containing tokens; these tokens will then be swapped out for data taken out of our database. Because these documents are written by users, I have little control over the casing of the tokens.

I found the beginning of a solution in the Microsoft.VisualBasic namespace. That library has a Strings toolbox, with methods that have historically been available in VB. The Strings.Replace method allows the coder to select either a binary (case sensitive) or text (case insensitive) search.

The first problem was that Strings.Replace is a toolbox method rather than an intrinsic one, which can make it awkward to use. It has a lot of parameters, most of which are not needed in most cases, so it has a different syntax than the standard Replace method. Lastly, a lot of C# coders do not like using the Visual Basic library. Writing an extension method lets you hide these issues. This is how it would look in VB:

VB.NET
Namespace MyExtensions
    <HideModuleName()> _
    Public Module ExtensionModule
        <System.Runtime.CompilerServices.Extension()> _
        Public Function MyReplace(ByVal Str As String, _
                                  ByVal Find As String, _
                                  ByVal Replacement As String) As String

            Return Microsoft.VisualBasic.Strings.Replace(Str, _
                                                         Find, _
                                                         Replacement, _
                                                         Compare:=CompareMethod.Text)

        End Function
    End Module
End Namespace

And in C#:

C#
namespace MyExtensions
{
    public static class ExtensionMethods
    {
        public static String MyReplace(this String Str, 
                                            String Find, 
                                            String Replacement)
        {
            return Microsoft.VisualBasic.Strings.Replace(Str, 
                                                         Find, 
                                                         Replacement, 
                                                         1, 
                                                         -1, 
                                                         CompareMethod.Text);
        }
    }
}

You will note that the first parameter holds the value of the object itself, in this case a String, which allows you to use it in your method.

If you are importing or using the MyExtensions namespace, IntelliSense will now show the method MyReplace on all String objects. In C#, the type string translates to the .NET String object, so your extension will be available from either type.

VB.NET
Dim S As String = "ABCDEFGHI"
Dim S1 = S.MyReplace("hi", "yz")

This code creates a String object named S and gives it a value. A second String object, S1, is created and assigned a value using the extension method. Because MyReplace performs a case insensitive replace, S1 will be assigned a value of "ABCDEFGyz".

You may have noticed that the above examples use value types for the first parameters. This is not a requirement, at least not in VB: by using a reference parameter, you can create extension methods that modify the calling object itself. Consider this method:

VB.NET
Namespace MyExtensions
    <HideModuleName()> _
    Public Module ExtensionModule
        <System.Runtime.CompilerServices.Extension()> _
        Public Sub MyReplaceRef(ByRef Str As String, _
                                ByVal Find As String, _
                                ByVal Replacement As String)

            Str = Microsoft.VisualBasic.Strings.Replace(Str, _
                                                        Find, _
                                                        Replacement, _
                                                        Compare:=CompareMethod.Text)
        End Sub

    End Module
End Namespace

Because the parameter Str is passed by reference, you can reassign its value. With this method, you can now do something like this:

VB.NET
Dim S As String = "ABCDEFGHI"
S.MyReplaceRef("hi", "yz")

After this code is run, S will have a value of "ABCDEFGyz".

I have not been able to write similar functionality in C#; the compiler does not like it when I pass the first parameter in as a pointer. Then again, I code VB for a living so I might be missing something obvious. If anyone knows how to do this, please let me know.

Extending Strings: Overloading

The above examples use distinct names for the extension methods. But what if I just wanted to call it Replace?

You can write extensions that overload existing methods, provided that your extensions have a different effective signature; do not count the first parameter which gives the object being extended. If you were to simply change MyReplace to Replace, your extension will not appear because its effective signature is String String, the same as one of the two built-in String.Replace methods. To make it available, you must change the signature. We can do this easily by adding a boolean parameter to show whether or not your replace will be case sensitive. This is what the revised method looks like:

VB.NET
Namespace MyExtensions
    <HideModuleName()> _
    Public Module ExtensionModule
        <System.Runtime.CompilerServices.Extension()> _
        Public Function Replace(ByVal Str As String, _
                                ByVal Find As String, _
                                ByVal Replacement As String, _
                                ByVal CaseSensitive as Boolean) As String

            If CaseSensitive Then
                Return Microsoft.VisualBasic.Strings.Replace(Str, _
                                                             Find, _
                                                             Replacement, _
                                                             Compare:=CompareMethod.Binary)
            Else
                Return Microsoft.VisualBasic.Strings.Replace(Str, _
                                                             Find, _ 
                                                             Replacement, _
                                                             Compare:=CompareMethod.Text)
            End If
        End Function

    End Module
End Namespace

And in C#:

C#
namespace MyExtensions
{
    public static class ExtensionMethods
    {
        public static String Replace(this String Str, 
                                            String Find, 
                                            String Replacement, 
                                            bool CaseSensitive)
        {
            if (CaseSensitive) 
                return Microsoft.VisualBasic.Strings.Replace(Str, 
                                                             Find, 
                                                             Replacement, 
                                                             1, 
                                                             -1, 
                                                             CompareMethod.Binary);
            else
                return Microsoft.VisualBasic.Strings.Replace(Str, 
                                                             Find, 
                                                             Replacement, 
                                                             1, 
                                                             -1, 
                                                             CompareMethod.Text);
        }
    }
}

Now, IntelliSense will show that there are three forms of String.Replace; the third one will have the three parameter signature and be flagged as an extension.

Overloaded.PNG

In Visual Basic, it is possible to write extension methods with optional parameters (C# does not support optional parameters.)

VB.NET
Namespace MyExtensions
    <HideModuleName()> _
    Public Module ExtensionModule
        <System.Runtime.CompilerServices.Extension()> _
        Public Function Replace(ByVal Str As String, _
                                ByVal Find As String, _
                                ByVal Replacement As String, _
                       Optional ByVal CaseSensitive as Boolean = False) As String

            If CaseSensitive Then
                Return Microsoft.VisualBasic.Strings.Replace(Str, _
                                                             Find, _
                                                             Replacement, _
                                                             Compare:=CompareMethod.Binary)
            Else
                Return Microsoft.VisualBasic.Strings.Replace(Str, _
                                                             Find, _ 
                                                             Replacement, _
                                                             Compare:=CompareMethod.Text)
            End If
        End Function

    End Module
End Namespace

This will compile just fine and appear through IntelliSense. You will run into trouble if you try to use it, however.

VB.NET
Dim S As String = "ABCDEFGHI"
Dim S1 = S.Replace("hi", "yz")

The problem is that the compiler checks the built-in methods first. If, and only if, none of the built-in methods match will it check for extensions.

In the above example, Replace is being called with a String String signature. Because this matches the signature of one of the built-in String.Replace methods, the compiler will use the build-in method and not your code. The moral is that if you are going to write extension methods with optional parameters, you should make sure that the required parameters make your methods' signatures distinct.

Beyond Strings

Of course, the techniques outlined above are not limited to just String objects. You could, say, write a DateTime extension which returns a frequently used text format:

VB.NET
<System.Runtime.CompilerServices.Extension()> _
Public Function ToStringAnsi(ByVal DT As DateTime) As String
    Return DT.ToString("yyyy-MM-dd")
End Function
C#
public static String ToStringAnsi(this DateTime DT)
{
    return DT.ToString("yyyy-MM-dd");
}

Or maybe you often need the inverse of a decimal variable:

VB.NET
<System.Runtime.CompilerServices.Extension()> _
Public Function Invert(ByVal D As Decimal) As Decimal
    Return 1 / D
End Function
C#
public static Decimal Invert(this Decimal D)
{
    return (1 / D);
}

Extending Generic Types

Any object can be extended, even generic objects. Suppose you wanted to create a Maximum method on a generic List that returns the item in the list with the maximum value. You start by declaring the extension itself to be generic, which brings your strong type into the picture. You can also apply constraints just as with any other generic declaration. Then, use the type to set up your other parameters and code away.

VB.NET
<System.Runtime.CompilerServices.Extension()> _
Public Function Maximum(Of T As IComparable)(ByVal L As List(Of T), _
                                             ByVal Comparer As IComparer(Of T)) As T
    If L Is Nothing Then Return Nothing

    Dim Max As T = L(0)

    For Each Item As T In L
        If Comparer.Compare(Item, Max) > 0 Then Max = Item
    Next

    Return Max
End Function
C#
public static T Maximum<T>(this List<T> L, IComparer<T> Comparer)
    where T: IComparable
{
    if (L == null) return default(T);

    T Max = L[0];
    foreach (T Item in L)
    {
        if (Comparer.Compare(Item, Max) > 0) Max = Item;
    }
    return Max;
}

In these code examples, note that T is required to implement IComparable; this will automate a lot of your safety checking. Because the extension is declared generic, you can pass T to the rest of your method, including the declarations of L and Comparer. The code verifies that there is, in fact, a valid List, makes a note of the first item in the list, and then calls Comparer.Compare on each item in the list, updating the temporary storage if a greater value is found. After iterating through the list, the temporary storage holds the maximum value, which is returned.

VB.NET
Public Sub Test()
    Dim L As New List(Of String)

    L.Add("Alpha")
    L.Add("Beta")
    L.Add("Gamma")
    L.Add("Delta")

    System.Diagnostics.Debug.WriteLine(L.Maximum(StringComparer.CurrentCulture))
End Sub
C#
public void Test()
    {
        List<String> L = new List<String>();
        L.Add("Alpha");
        L.Add("Beta");
        L.Add("Gamma");
        L.Add("Delta");

        System.Diagnostics.Debug.WriteLine(L.Maximum(StringComparer.CurrentCulture));
    }

StringComparer provides several comparison methods for String objects; CurrentCulture performs a case sensitive string comparison based on the workstation's cultural settings. Assuming an English based setting, these examples will return "Gamma".

There an odd difference between the VB and C# versions that should be noted. The VB method can return Nothing; generics in C# apparently cannot return null, so if L == null I have the method return the default value of T.

You can also extend objects that have two or more generic parameters, such as Dictionary(Of T1, T2).

Extending Interfaces

Strictly speaking, the objects you extend do not have to be objects at all: you can extend interfaces, too. Suppose that you want a Maximum method on all objects that implement ICollection and not just on List objects. You can do this very easily by replacing List with ICollection, like so:

VB.NET
<System.Runtime.CompilerServices.Extension()> _
Public Function Maximum(Of T As IComparable)(ByVal IC as ICollection(Of T), _
                                             ByVal Comparer As IComparer(Of T)) As T
    If IC Is Nothing Then Return Nothing

    Dim Max As T = IC(0)

    For Each Item As T In IC
        If Comparer.Compare(Item, Max) > 0 Then Max = Item
    Next

    Return Max
End Function
C#
public static T Maximum<T>(this ICollection<T> IC, IComparer<T> Comparer)
    where T: IComparable
{
    if (L == null) return default(T);

    T Max = default(T);
    foreach (T Item in IC)
    {
        if (Comparer.Compare(Item, Max) > 0) Max = Item;
    }
    return Max;
}

Now, Maximum is available on lists, dictionaries, arrays and any other object that implements ICollection, even when these objects are created intrinsically.

Interface.PNG

Here again, C# imposes a constraint not found in VB. In Visual Basic, I can set Max to the first item in the collection; C# does not allow this, so Max is initialized with default(T).

There is no requirement that your interfaces be generic, of course.

Best Practices

Extension methods are a very powerful tool, and with great power comes great responsibility. In 2007, the Visual Basic Team at Microsoft published a blog article titled Extension Methods Best Practices, available here. If you are going to be doing a lot with this technique, it might be good to review these suggestions.

History

  • 20th March, 2009: Initial post
  • 31st March, 2009: Cleaned up and clarified the text and fixed some formatting issues.
  • 1st April, 2009: Some minor corrections.

License

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