Tuesday, 18 March 2008

Extending Base Type Functionality with Extension Methods

 

Introduction
In November 2007, Microsoft released the .NET Framework version 3.5, Visual Studio 2008, and new versions of the C# and Visual Basic languages (see An Overview of ASP.NET 3.5 and Visual Studio 2008 for more details on the release). The new C# and Visual Basic versions include a myriad of exciting new features that make the languages more expressive and open the door for new syntax constructs, like LINQ. One of these new language features is extension methods, which is the topic for today's article.

Extension methods allow a developer to tack on her own methods to an existing class in the .NET Framework. For example, imagine that our developer created a method named StripHtml, that strips HTML elements from a string using a regular expression. By associating this method with the System.String class, it could be called as if it was one of the System.String class's built-in methods:

' VB
Dim str As String = "<b>Hello, world!</b>"
Dim strippedString = str.StripHtml()

// C#
string str = "<b>Hello, world!</b>";
string strippedString = str.StripHtml();

Extension methods make it easy to add functionality to an existing type using a natural syntax. Also, since the extension methods appear in Visual Studio's IntelliSense drop-down list, they are easier to find and use than wrapper class equivalents. In this article we will see how to create extension methods in both C# and Visual Basic. Read on to learn more!

The Impetus Behind Extension Methods
Over the past several years, I've worked with a number of clients in building large ASP.NET web applications. In most of these applications there has been a need to define extra functionality on strings. On some pages we need to strip all of the HTML elements from a string before displaying it; on other pages we need to parse the string and censor out an potentially profane words; and on other pages we need to convert URLs into appropriate HTML anchor tags (i.e., converting the text URL into <a href="URL">URL</a>).

To provide this function I created a wrapper class named StringHelpers with methods for stripping HTML elements, censoring profanity, and converting URLs into hyperlinks. The wrapper class was implemented as a class with a series of static methods that accepted a string as input, perform the intended operation, and then return the resulting string.

' VB
Public Class StringHelpers
   Public Shared Function StripHtml(ByVal str As String) As String
      ... Strip the HTML, and return the stripped string ...
   End Function

   Public Shared Function RemoveProfanity(ByVal str As String) As String
      ... Remove the profanity, and return the "clean" string ...
   End Function

   Public Shared Function ConvertUrlsToHyperlinks(ByVal str As String) As String
      ... Replace URLs with the appropriate HTML ...
   End Function
End Class


// C#
public class StringHelpers
{
   public static string StripHtml(string str)
   {
      ... Strip the HTML, and return the stripped string ...
   }

   public static string RemoveProfanity(string str)
   {
      ... Remove the profanity, and return the "clean" string ...
   }

   public static string ConvertUrlsToHyperlinks(string str)
   {
      ... Replace URLs with the appropriate HTML ...
   }
}

With the StringHelpers wrapper class in place, I can exercise this added string functionality using code like:

' VB
Dim str As String = "<b>Hello, world!</b>"
Dim strippedString = StringHelpers.StripHtml(str)

// C#
string str = "<b>Hello, world!</b>";
string strippedString = StringHelpers.StripHtml(str);

While wrapper classes offer a simple approach to encapsulate commonly used functionality, there is no tight association between the wrapper class's functionality and the type its methods operate on. In order to use the wrapper class, a developer must first know of its existence; he must dig into the various methods to see how they operate. Ideally, these string-specific methods (StripHtml, RemoveProfanity, and ConvertUrlsToHyperlinks) would be part of the System.String type, and I could call them using a more intuitive syntax (StringVariable.StripHtml()) and enjoy IntelliSense support through Visual Studio. Extension methods provide these advantages, allowing a developer to more intuitively assign methods to a particular type.

Extending Type Functionality via Inheritance
In addition to wrapper classes, another common approach to adding functionality to an existing type is through inheritance. Inhertiance is a core object-oriented programming principle that enables a developer to take an existing type and extend its functionality by adding new methods, properties, and events. The downside with inheritance is that you need to create a new type to extend the existing type. Consequently, in order to use the added functionality you have to work with the new type. This is a Good Thing and appropriate design in most cases, but if you just want to add a simple method or two to add new functionality to an existing type, having to use the newly created type can be burdensome. Moreover, not all types can be extended. The System.String class, for example, is marked as sealed, meaning that it cannot be extended.

For more on using inheritance, see Ian Stallings article, Using Object-Orientation in ASP.NET: Inheritance.

Creating Extension Methods for System.DateTime
Let's look at how to create and use extension methods. Many online messageboard sites - like the ASP.NET Forums - display the times the messages were posted using descriptions relative to the current date and time (rather than simply displaying the full date and time). For example, if a post was made within the last minute, the post's date value might read: "Seconds ago." Alternatively, if the post happened in the past few minutes, the message might read: "3 minutes ago." If the post happened within the current date, it might just show the time, like: "10:34 AM". If the post happened in a prior day, then the entire date and time would be displayed: "December 3rd, 2007 10:34 AM."

The System.DateTime class has a number of formatting methods - ToString(), ToShortDateString(), and ToLongTimeString(), to name a few - but wouldn't it be nice to have a method like ToRelativeToCurrentTimeString() method that effected the logic described above? We can add such a method using extension methods. Let's create two such extension methods on the System.DateTime class: ToRelativeToCurrentTimeString() and ToRelativeToCurrentUtcTimeString(). These methods will both operate on a DateTime instance and return a string that displays a time description relative to the current time (or UTC time).

Since the syntax for creating extension methods differs quite a bit between VB and C#, let's examine first implement these extension methods using VB; after doing so, we will create the extension methods in C#. The download at the end of this article includes two ASP.NET website applications, either of which you can open with Visual Studio 2008: one in VB and one in C#.

Creating the Extension Methods with Visual Basic
In order to create the extension methods in Visual Basic we need to first create a Module. For each extension method you want to create, add a method whose first input parameter is of the type that you want to add the extension method to. Moreover, prefix the method with the Extension() attribute.

The following Module named DateTimeHelpers contains two methods: ToRelativeToCurrentTimeString(DateTime) and ToRelativeToCurrentUtcTimeString(DateTime), both of which accept a DateTime instance as their first input parameter. The methods are also marked with the Extension() attribute (which is found in the System.Runtime.CompilerServices namespace). The two methods call the private ToRelativeString method, which returns the appropriate string message based on the difference in time between the two passed-in DateTime values.

Imports System.Runtime.CompilerServices
Imports Microsoft.VisualBasic

Namespace Helpers
    Public Module DateTimeHelpers
       <Extension()> _
       Public Function ToRelativeToCurrentTimeString(ByVal dt As DateTime) As String
            Return ToRelativeString(dt, DateTime.Now)
       End Function

       <Extension()> _
       Public Function ToRelativeToCurrentUtcTimeString(ByVal dt As DateTime) As String
            Return ToRelativeString(dt, DateTime.UtcNow)
       End Function

       Private Function ToRelativeString(ByVal timeInPast As DateTime, ByVal currentTime As DateTime) As String
            If timeInPast.Date <> currentTime.Date Then
                ' timeInPast happend more than a day ago... show the date & time
                Return timeInPast.ToString()
            Else
                ' timeInPast and currentTime happened on the same day...
                Dim secondsApart As Integer = Convert.ToInt32(currentTime.Subtract(timeInPast).TotalSeconds)

                ' See if the date dt is within the last hour...
                If secondsApart < 10 Then
                   Return "Seconds ago..."
                ElseIf secondsApart < 60 Then
                   Return "Less than a minute ago..."
                ElseIf secondsApart < 3600 Then
                   Return String.Format("{0:N0} minutes ago...", secondsApart / 60 + 1)
                End If

                ' Ok, the date is more than an hour old... show the time
                Return timeInPast.ToShortTimeString()
            End If
       End Function
    End Module
End Namespace

We can now call these extension methods from a DateTime instance. In order to use an extension method, we first need to add an Imports directive to the code file, importing the namespace where the extension methods reside (Helpers). Upon doing that, the extension method is visible in the IntelliSense drop-down list, as the following screen shot illustrates.

The download at the end of this article includes a demo that allows the visitor to enter a date and time value into a TextBox. On postback, the entered value is converted into a DateTime and the return value from the ToRelativeToCurrentTimeString() method is displayed in a Label Web control.

Extension Methods with Input Parameters
The ToRelativeToCurrentTimeString() and ToRelativeToCurrentUtcTimeString() methods are defined in the Module as having one input parameter of type DateTime. This results in an extension method applied to the DateTime type that has no input parameters. But what if we needed to pass in one or more parameters to the ToRelativeToCurrentTimeString()? For example, maybe we wanted to pass in some integer value like so: DateTimeInstance.ToRelativeToCurrentTimeString(someIntegerValue).

To accomplish this, simply update the definition of the ToRelativeToCurrentTimeString() method so that it takes in two input parameters: a DateTime first and then an Integer like so:

<Extension()> _
Public Function ToRelativeToCurrentTimeString(ByVal dt As DateTime, ByVal i As Integer) As String
   ...
End Function

The order of the input parameter's are important with extension methods, because the first parameter is the type that the extension method is applied to. In other words, if you redefined the extension method so that the Integer parameter was the first input parameter, and the DateTime the second, then the resulting extension method would be applied to objects of type Integer.

Creating the Extension Methods with C#
The syntax for creating the extension methods in C# is a little different than in Visual Basic. The equivalent of a Module in C# is a static class. Also, the methods in a static class must be marked as static, as well. Finally, instead of using the Extension() attribute, C# requires that the this keyword be used for the input parameter whose type the extension method will be applied.

using System;

namespace Helpers
{
    public static class DateTimeHelpers
    {
       public static string ToRelativeToCurrentTimeString(this DateTime dt)
       {
            return ToRelativeString(dt, DateTime.Now);
       }

       public static string ToRelativeToCurrentUtcTimeString(this DateTime dt)
       {
            return ToRelativeString(dt, DateTime.UtcNow);
       }

       private static string ToRelativeString(DateTime timeInPast, DateTime currentTime)
       {
            if (currentTime.Date != timeInPast.Date)
                // dt happend more than a day ago... show the date & time
                return timeInPast.ToString();
            else
            {
                // dt and rightNow happened on the same day...
                int secondsApart = Convert.ToInt32(currentTime.Subtract(timeInPast).TotalSeconds);

                // See if the date dt is within the last hour...
                if (secondsApart < 10)
                   return "Seconds ago...";
                else if (secondsApart < 60)
                   return "Less than a minute ago...";
                else if (secondsApart < 3600)
                   return string.Format("{0:N0} minutes ago...", secondsApart / 60 + 1);

                // Ok, the date is more than an hour old... show the time
                return timeInPast.ToShortDateString();
            }
       }
    }
}

With the extension methods defined, they can be used in code as if they were native members on the underlying type. Like with the Visual Basic example, in order to use the extension methods in a code file you must import the appropriate namespace (Helpers) via the using statement (i.e., using Helpers;).

Things to Remember When Using Extension Methods
When using extension methods there are a few things to keep in mind. First, realize that extension methods are simply syntactic sugar - they do not introduce any new functionality, but just provide existing functionality in a more human-friendly syntax. Underneath the covers, the design-time experience allows for the IntelliSense functionality, but in actuality an extension method call is compiled into a call to the appropriate method in the static class or Module. In other words, extension methods are synonymous with wrapper classes - the only difference is the design-time syntax.

Since extension methods are functionality equivalent to wrapper classes, the same limitations apply. A wrapper class, for instance, can only access public members of the types it is working with. Likewise, an extension method defined on a type can only work with those types public members. It cannot read or write protected members (which is possible with inheritance). Moreover, extension methods can only be applied to methods - there is no such thing as an extension property or an extension event.

Finally, don't forget that in order to use an extension method you must import the appropriate namespace into your code. In VB that means you'll need to do an Import namespace. In C# it's using namespace;.

Conclusion
In this article we looked at a new feature in C# 3.0 and Visual Basic 9.0: extension methods. We saw how to create and use a simple extension method in both C# and VB. The download at the end of this article includes the code examined in this article along with a simple demo application illustrating the extension method is use.

Happy Programming!

  • By Scott Mitchell
  • Source: http://aspnet.4guysfromrolla.com/articles/120507-1.aspx#postadlink

    No comments: