Extension methods sind doch irgendwie cool

February 28, 2008 at 2:49 PMAndre Loker

Eigentlich stand ich den Extension Methods von C# 3.0 ja kritisch gegenüber, aber irgendwie sind sie doch ziemlich cool :-)

Hier ein kleines Beispiel aus der Praxis.

   1: /// <summary>
   2: /// Provides extension methods for <see cref="string"/>
   3: /// </summary>
   4: public static class StringExtensions {
   5:     /// <summary>
   6:     /// Calls ToString on the given object if <paramref name="obj"/> is not null; otherwise, 
   7:     /// <see cref="string.Empty"/> is returned.
   8:     /// </summary>
   9:     /// <param name="obj">The obj.</param>
  10:     /// <returns>The result of <see cref="object.ToString()"/> invoked on <paramref name="obj"/> if
  11:     /// <paramref name=" obj"/> is not null; otherwise, <see cref="string.Empty"/></returns>
  12:     public static string ToSafeString<T>( this T obj ) {
  13:         if ( typeof( T ).IsValueType ) {
  14:             return obj.ToString();
  15:         }
  16:         return Equals( obj, null ) ? string.Empty : obj.ToString();
  17:     }
  18:  
  19:     /// <summary>
  20:     /// Formats a string using the given format and a value.
  21:     /// </summary>
  22:     /// <param name="format">The format.</param>
  23:     /// <param name="value">The value.</param>
  24:     /// <returns>The formatted string</returns>
  25:     public static string Formatted<T>( this string format, T value )
  26:         where T : struct {
  27:         return string.Format( format, value.ToSafeString() );
  28:     }
  29:  
  30:     /// <summary>
  31:     /// Formats a string using the given format and the given values
  32:     /// </summary>
  33:     /// <typeparam name="T1">The type of the first argument.</typeparam>
  34:     /// <typeparam name="T2">The type of the second argument.</typeparam>
  35:     /// <param name="format">The format.</param>
  36:     /// <param name="value1">The first value.</param>
  37:     /// <param name="value2">The second value.</param>
  38:     /// <returns>The formatted string</returns>
  39:     public static string Formatted<T1, T2>( this string format, T1 value1, T2 value2 )
  40:         where T1 : struct
  41:         where T2 : struct {
  42:         return string.Format( format, value1.ToSafeString(), value2.ToSafeString() );
  43:     }
  44:  
  45:     /// <summary>
  46:     /// Formats a string using the given format and the given values
  47:     /// </summary>
  48:     /// <typeparam name="T1">The type of the first argument.</typeparam>
  49:     /// <typeparam name="T2">The type of the second argument.</typeparam>
  50:     /// <typeparam name="T3">The type of the third argument.</typeparam>
  51:     /// <param name="format">The format.</param>
  52:     /// <param name="value1">The first value.</param>
  53:     /// <param name="value2">The second value.</param>
  54:     /// <param name="value3">The third value.</param>
  55:     /// <returns>The formatted string</returns>
  56:     public static string Formatted<T1, T2, T3>( this string format, T1 value1, T2 value2, T3 value3 )
  57:         where T1 : struct
  58:         where T2 : struct
  59:         where T3 : struct {
  60:         return string.Format( format, value1.ToSafeString(), value2.ToSafeString(), value3.ToSafeString() );
  61:     }
  62:  
  63:     /// <summary>
  64:     /// Formats a string using the given format and the given values.
  65:     /// </summary>
  66:     /// <param name="format">The format.</param>
  67:     /// <param name="values">The valuess.</param>
  68:     /// <returns>The formatted string</returns>
  69:     public static string Formatted( this string format, object[] values ) {
  70:         return string.Format( format, values );
  71:     }
  72: }

Als Anwendungsbeispiele einige Unit-Tests:

   1: [TestFixture]
   2: public class StringExtensionsTests {
   3:     [Test]
   4:     public void CanFormat() {
   5:         var format = "{0} + {1}";
   6:         var result = format.Formatted( 3, 4 );
   7:         var oldVersion = string.Format( format, 3, 4 );
   8:         Assert.AreEqual( "3 + 4", result );
   9:         Assert.AreEqual( oldVersion, result );
  10:     }
  11:  
  12:     [Test]
  13:     public void ToSafeStringHandlesNull() {
  14:         object x = null;
  15:         Assert.AreEqual( string.Empty, x.ToSafeString() );
  16:     }
  17:  
  18:     [Test]
  19:     public void ToSafeStringHandlesValueType() {
  20:         Assert.AreEqual( "5", 5.ToSafeString() );
  21:     }
  22:  
  23:     [Test]
  24:     public void ToSafeStringHandlesRefType() {
  25:         Assert.AreEqual( "foobar", "foobar".ToSafeString() );
  26:     }
  27: }

Zum einen ist der alternative Syntax kürzer. Es mag hier noch Geschmackssache sein, welche Variante (statische Format-Methode ggü. Extension Method). Die Variante mit den Extension Methods ist allerdings auch ein wenig effizienter. Da die Formatted mit einem, zwei oder drei Parametern generisch sind, findet kein Boxing bei der Übergabe der Argumente statt.

HTML Strings

Bei der Ausgabe von Strings in einem HTML Dokument sollte der Text allein schon aus Sicherheitsgründen HTML-codiert werden, besonders, wenn es sich dabei um Benutzereingaben handelt. Dazu steht die Methode HtmlEncode von HttpUtility bzw. HttpServerUtility bereit. Da ich es lästig finde, stets Server.HtmlEncode(derText) bzw.gar HttpUtility.HtmlEncode(derText) zu verwenden, habe ich eine Klasse geschrieben, die HtmlEncode als Extension-Methode der String-Klasse hinzufügt. Das ganze habe ich noch erweitert mit

  • einem Pendant für UrlEncode
  • den Gegenstücken HtmlDecode und UrlDecode
  • der Methode ToHtmlString, einer Extension für Object, die die Ausgabe von ToString() Html-kodiert (bzw. String.Empty zurückgibt bei einer verwendung mit null)
  • dem ToHtmlString-Pendant für URLs: ToUrlString

Der Code:

   1: /// <summary>
   2: /// Extension methods for <see cref="string"/> and <see cref="object"/> that are useful in the context
   3: /// of web applications.
   4: /// </summary>
   5: public static class WebStringExtensions {
   6:     /// <summary>
   7:     /// HTML encodes the given string.
   8:     /// </summary>
   9:     /// <param name="text">The text.</param>
  10:     /// <returns>The HTML encoded version of <paramref name="text"/></returns>
  11:     public static string HtmlEncode( this string text ) {
  12:         return HttpUtility.HtmlEncode( text );
  13:     }
  14:  
  15:     /// <summary>
  16:     /// Decodes the given HTML encoded string.
  17:     /// </summary>
  18:     /// <param name="text">The text.</param>
  19:     /// <returns>The plain version of the HTML encoded <paramref name="text"/></returns>
  20:     public static string HtmlDecode( this string text ) {
  21:         return HttpUtility.HtmlDecode( text );
  22:     }
  23:  
  24:     /// <summary>
  25:     /// Calls <see cref="object.ToString()"/> on the given object and HTML encodes the result.
  26:     /// </summary>
  27:     /// <param name="item">The item.</param>
  28:     /// <returns>
  29:     /// The HTML encoded version of <paramref name="item"/> or <see cref="string.Empty"/> if
  30:     /// <paramref name="item"/> was null.
  31:     /// </returns>
  32:     public static string ToHtmlString<T>( this T item ) {
  33:         return item.ToSafeString().HtmlEncode();
  34:     }
  35:  
  36:     /// <summary>
  37:     /// Encodes a string to be used inside an url.
  38:     /// </summary>
  39:     /// <param name="text">The text.</param>
  40:     /// <returns>The URLs encoded version of <paramref name="text"/></returns>
  41:     public static string UrlEncode( this string text ) {
  42:         return HttpUtility.UrlEncode( text );
  43:     }
  44:  
  45:     /// <summary>
  46:     /// Decodes a string that was url encoded.
  47:     /// </summary>
  48:     /// <param name="text">The text.</param>
  49:     /// <returns>The plain version of the URL encoded <paramref name="text"/></returns>
  50:     public static string UrlDecode( this string text ) {
  51:         return HttpUtility.UrlDecode( text );
  52:     }
  53:  
  54:     /// <summary>
  55:     /// Calls <see cref="object.ToString()"/> on the given object and HTML encodes the result.
  56:     /// </summary>
  57:     /// <param name="item">The item.</param>
  58:     /// <returns>
  59:     /// The URL encoded version of <paramref name="item"/> or <see cref="string.Empty"/> if
  60:     /// <paramref name="item"/> was null.
  61:     /// </returns>
  62:     public static string ToUrlString<T>( this T item ) {
  63:         return item.ToSafeString().UrlEncode();
  64:     }
  65: }

Hier einige Unit-Tests die direkt die Verwendung der Methoden zeigen:

   1: [TestFixture]
   2: public class WebStringExtensionsTests {
   3:     [Test]
   4:     public void CanHtmlEncode() {
   5:         var original = "für";
   6:         var encoded = original.HtmlEncode();
   7:         var expectedEncoded = "f&#252;r";
   8:         Assert.AreEqual( expectedEncoded, encoded );
   9:     }
  10:  
  11:     [Test]
  12:     public void CanHtmldecode() {
  13:         var original = "f&#252;r";
  14:         var decoded = original.HtmlDecode();
  15:         var expectedDecoded = "für";
  16:         Assert.AreEqual( expectedDecoded, decoded );
  17:     }
  18:  
  19:     [Test]
  20:     public void CanUrlEncode() {
  21:         var original = "für";
  22:         var encoded = original.UrlEncode();
  23:         var expectedEncoded = "f%c3%bcr";
  24:         Assert.AreEqual( expectedEncoded, encoded );
  25:     }
  26:  
  27:     [Test]
  28:     public void CanUrlDecode() {
  29:         var original = "f%c3%bcr";
  30:         var decoded = original.UrlDecode();
  31:         var expectedDecoded = "für";
  32:         Assert.AreEqual( expectedDecoded, decoded );
  33:     }
  34:  
  35:     [Test]
  36:     public void CanUseToHtmlString() {
  37:         var text = new MyCustomObject( 1, 2 ).ToHtmlString();
  38:         Assert.AreEqual( "a &lt;= b", text );
  39:         text = new MyCustomObject( 2, 1 ).ToHtmlString();
  40:         Assert.AreEqual( "a &gt; b", text );
  41:     }
  42:  
  43:     [Test]
  44:     public void CanHandleNullInToHtmlString() {
  45:         object o = null;
  46:         var text = o.ToHtmlString();
  47:         Assert.AreEqual( string.Empty, text );
  48:     }
  49:  
  50:     [Test]
  51:     public void CanUseToUrlString() {
  52:         var text = new MyCustomObject( 1, 2 ).ToUrlString();
  53:         Assert.AreEqual( "a+%3c%3d+b", text );
  54:         text = new MyCustomObject( 2, 1 ).ToUrlString();
  55:         Assert.AreEqual( "a+%3e+b", text );
  56:     }
  57:  
  58:     [Test]
  59:     public void CanHandleNullInToToUrlString() {
  60:         object o = null;
  61:         var text = o.ToUrlString();
  62:         Assert.AreEqual( string.Empty, text );
  63:     }
  64:  
  65:     #region Nested type: MyCustomObject
  66:     private class MyCustomObject {
  67:         private readonly int a;
  68:         private readonly int b;
  69:  
  70:         public MyCustomObject( int a, int b ) {
  71:             this.a = a;
  72:             this.b = b;
  73:         }
  74:  
  75:         public override string ToString() {
  76:             return a > b ? "a > b" : "a <= b";
  77:         }
  78:     }
  79:     #endregion
  80: }

Auch hier arbeiten wir wieder weitestgehend Resourcen schonend. Gerade ToHtmlString und ToUrlString sind im Zuge von Databinding extrem nützlich:

   1: <asp:Label runat="server" ID="label" Text='<%# Eval("UserName").ToHtmlString() %>' />
   2: <asp:HyperLink runat="server" ID="link" NavigateUrl='<%# string.Format("~/Profile.aspx?user={0}", Eval("LoginName").ToUrlString()) %>' />

Posted in: Snippets

Tags: