Introduction to language extension method

Developers are always looking for new ways to optimize their code and surprisingly extension methods are used usually when a developer doesn't realize they are using them and that they can write their own language extensions. Let's dig in.

What are extension methods?

Extension methods first appeared with .NET Framework 3.5 which in short allow existing classes and types to be extended. Extension methods can be very useful and make code method based, easily repeatable especially when working with many projects and have these extension methods in a class project.

If you coding now with a Framework 3.5 or higher more likely than not you are using extension methods fairly often e.g. converting an array to a list via .ToList() which is a generic extension method meaning .ToList() works on strings, numerics etc. or with Entity Framework Core there is Like.

When should you use extension methods?

The first reason is when a developer is frequently writing custom functions that are either used often or how to write a function is not remembered. 

For example, a simple example would be to create a string delimited by a specific type such as a list of integers which for many is simple but not for every developer. 

A more complex example, a developer has written a SQL statement with parameters (no matter the data provider) in code and has a syntax error or the query functions without errors but returns unexpected results. Code to recreate the SQL statement with values for the parameters rather than placeholders is quite a bit of code and not needed often. There could be a language extension (and there is one in the sample source along with this article) that works off the command object to create a string representation of the query with values which in turn can be pasted into a database editor such as SSMS (SQL-Management Studio), Toad for Oracle or directly into a MS-Access database new query.

Other reasons, extending a component or control along with taking a .NET Framework function and creating a method-based version.

Consider a undue for Entity Framework, looks great but does it really make for a good language extension? For some it may while this same code can be done as a method call. 

public  class EntityFrameworkUtilities
{
    /// <summary>
    /// Provides the ability to revert changes in a
    /// live DbContext, not disconnected context.
    /// </summary>
    /// <param name="context"></param>
    /// <param name="entity"></param>
    public void UndoingChangesDbEntityPropertyLevel(DbContext context, object entity)
    {
        DbEntityEntry entry = context.Entry(entity);
        if (entry.State == EntityState.Added || entry.State == EntityState.Detached)
        {
            entry.State = EntityState.Detached;
            return;
        }
        if (entry.State == EntityState.Deleted)
        {
            entry.Reload();
        }
 
        foreach (string propertyName in entry.OriginalValues.PropertyNames)
        {
            // Get and Set the Property value by the Property Name.   
            entry.Property(propertyName).CurrentValue = entry.Property(propertyName).OriginalValue;
        }
    }
}

What makes for a good extension method?

One case is for code simply to be hidden out of your main business logic code or something not used much but when needed rather than search the web have the code in a class project that can be added to a Visual Studio solution, add a reference to your project and use it.

Couple this by documenting the extension methods using SandCastle so you know what's available.

To get started here are a couple of simple extension methods to removed all characters or numbers from a string.

using System.Text.RegularExpressions;
 
namespace ExtensionsLibrary
{
    public static class ConverterExtensions
    {
        public static string StripperNumbers(this string text) =>  
            Regex.Replace(text, "[^A-Za-z]""");
 
        public static bool AlphaToInteger(this string text, ref int result)
        {
            var value = Regex.Replace(text, "[^0-9]""");
            return int.TryParse(value, out result);
        }
 
    }
}

Then there are generic extensions, for example, determine if a date is between two dates or a int is between (in a range) to int values.

namespace ExtensionsLibrary
{
    public static class GenericExtensions
    {
        /// <summary>
        /// Determine if T is between lower and upper
        /// </summary>
        /// <typeparam name="T">Data type</typeparam>
        /// <param name="sender">Value to determine if between lower and upper</param>
        /// <param name="minimumValue">Lower of range</param>
        /// <param name="maximumValue">Upper of range</param>
        /// <returns>True if in range, false if not in range</returns>
        /// <example>
        /// <code>
        /// var startDate = new DateTime(2018, 12, 2, 1, 12, 0);
        /// var endDate = new DateTime(2018, 12, 15, 1, 12, 0);
        /// var theDate = new DateTime(2018, 12, 13, 1, 12, 0);
        /// Assert.IsTrue(theDate.Between(startDate,endDate));
        /// </code>
        /// </example>
        public static bool Between<T>(this IComparable<T> sender, T minimumValue, T maximumValue)
        {
            return sender.CompareTo(minimumValue) >= 0 && sender.CompareTo(maximumValue) <= 0;
        }
        /// <summary>Determines if a value is between two values, exclusive.</summary>
        /// <param name="sender">The source value.</param>
        /// <param name="minimumValue">The minimum value.</param>
        /// <param name="maximumValue">The Maximum value.</param>
        /// <returns><see langword="true"/> if the value is between the two values, exclusive.</returns>
        public static bool BetweenExclusive<T>(this IComparable<T> sender, T minimumValue, T maximumValue)
        {
            return sender.CompareTo(minimumValue) > 0 && sender.CompareTo(maximumValue) < 0;
        }
    }
}

Then there are vague but useful utility extensions such as given a string array possible from reading a text file where the expectation is comma separated values are expected to be int but may not be. We can assert they are using the following extension method.

public static bool AllInt(this string[] sender) =>
    sender.SelectMany(item => item.ToCharArray()).All(char.IsNumber);

Or simply read each line and do a conversion dam the torpedoes.

public static int[] ToIntegerPreserveArray(this string[] sender)
{
 
    var intArray = Array.ConvertAll(sender, (input) =>
    {
        int.TryParse(input, out var integerValue);
        return integerValue;
    });
 
    return intArray;
 
}

Or perhaps there is a need to figure out which values are not int by getting their index in the array.

public static int[] GetNonIntegerIndexes(this string[] sender)
{
    return sender.Select(
        (item, index) => int.TryParse(item, out var _) ? new
        {
            IsInteger = true,
            Index = index
        } : new
        {
            IsInteger = false,
            Index = index
        })
        .ToArray()
        .Where(item => item.IsInteger == false)
        .Select(item => item.Index)
        .ToArray();
}

As you can see there are many uses for extension methods. Two extensions I've used a lot.

The first given a string value say CompanyName produces Company Name.

public static class StringExtensions
{
    public static string SplitCamelCase(this string sender)
    {
        return Regex.Replace(Regex.Replace(sender, 
            "(\\P{Ll})(\\P{Ll}\\p{Ll})""$1 $2"), 
            "(\\p{Ll})(\\P{Ll})""$1 $2");
    }
}

Then for performing asynchronous work in Windows Forms.

public static void InvokeIfRequired<T>(this T control, Action<T> action) 
    where T : ISynchronizeInvoke
{
    if (control.InvokeRequired)
    {
        control.Invoke(new Action(() => action(control)), null);
    }
    else
    {
        action(control);
    }
}

The following is called in a async method where without InvokeIfRequired I'd need to write out the if condition and action.

private void FileOperations_OnMonitorFileMove(Delegates.MonitorArgs args)
{
 
    MonitorListBox.InvokeIfRequired(listBox => { listBox.Items.Add(args.Filename); });
    ChunkListBox.InvokeIfRequired(listBox =>
    {
        var index = listBox.FindString(args.Filename);
        if (index != -1)
        {
            listBox.Items.RemoveAt(index);
        }
    });
}

Language extensions work with .NET Framework, your code and third party libraries. For instance the following extension method is for GemBox SpreadSheet library.

Note [DebuggerStepThrough] attribute which instructs the debugger to step through that code which can be useful when debugging a lot of code.

/// <summary>
/// Remove double spaces and trim start and end of the value
/// </summary>
/// <param name="sender"></param>
/// <returns></returns>
[DebuggerStepThrough]
public static string CompressedValue(this ExcelCell sender) => 
    Regex.Replace(sender.StringValue, @"\s+"" ").Trim();

Unit testing

An important thing to do when creating your own language extension methods is not to simply try them out in your code but instead write unit test to validate they work.

In the supplied GitHub repository there is a unit test project to use as a starting point to write your own test.

And with that I invite you to try out my basic language extensions which require .NET Framework 3.5 or higher. Code was written in Visual Studio 2017.

Github repository



Comments

Popular posts from this blog

VB.NET Working with Delegate and Events

Coders asking questions in online forums

GitHub download partial repository