Some thoughts on global extension methods

New: Dynamically evaluate C# expressions and execute C# scripts with a single statement, from anywhere in a .NET application. Click here for more info.

In the past, I’ve sometimes found it useful to place some very generic extension methods in the global namespace, and/or at a visibility level corresponding to the classes extended. This can ease reuse, as the extension methods don’t require developers to take extra steps or have extra knowledge to use them; they’re just immediately available in type-ahead. Anyone who has suffered through using a namespace-happy API with multiple locations for extension methods may thank you for this approach.

As a brief example, consider the extension methods below. These mainly provide a bit of syntactic sugar for working with regular expressions, by converting what would often be multi-line calls into a single expression. The hiding of the System.Text.RegularExpressions namespace would be complete, except that in a few cases Match objects are exposed, as useful bundles of values and indices in the original string; System.Text.RegularExpressions would never need to be explicitly used even with those methods, if the var keyword were used.

using System.Collections.Generic;
using System.Text.RegularExpressions;

public static partial class StringExtensions
{
    private static MatchCollection EmptyMatchCollection = Regex.Matches("", "0");

    private static Match UnsuccessfulMatch = Regex.Match("a", "b");

    public static bool IsMatch(this string s, string pattern, bool isCompiled = false, bool ignoreCase = false)
    {
        if (s == null || pattern == null) return false;
        RegexOptions options = RegexOptions.CultureInvariant;
        if (ignoreCase)
            options |= RegexOptions.IgnoreCase;
        if (isCompiled)
            options |= RegexOptions.Compiled;
        return Regex.IsMatch(s, pattern, options);
    }

    public static int IndexOfPattern(this string s, string pattern, bool isCompiled = false, bool ignoreCase = false)
    {
        if (s == null || pattern == null) return -1;
        RegexOptions options = RegexOptions.CultureInvariant;
        if (ignoreCase)
            options |= RegexOptions.IgnoreCase;
        if (isCompiled)
            options |= RegexOptions.Compiled;
        Match match = Regex.Match(s, pattern, options);
        return match.Success ? match.Index : -1;
    }

    public static int LastIndexOfPattern(this string s, string pattern, bool isCompiled = false, bool ignoreCase = false)
    {
        if (s == null || pattern == null) return -1;
        RegexOptions options = RegexOptions.CultureInvariant | RegexOptions.RightToLeft;
        if (ignoreCase)
            options |= RegexOptions.IgnoreCase;
        if (isCompiled)
            options |= RegexOptions.Compiled;
        Match match = Regex.Match(s, pattern, options);
        return match.Success ? match.Index : -1;
    }

    public static MatchCollection Matches(this string s, string pattern, bool isCompiled = false, bool ignoreCase = false)
    {
        if (s == null || pattern == null) return EmptyMatchCollection;

        RegexOptions options = RegexOptions.CultureInvariant;
        if (ignoreCase)
            options |= RegexOptions.IgnoreCase;
        if (isCompiled)
            options |= RegexOptions.Compiled;

        return Regex.Matches(s, pattern, options);
    }

    public static IEnumerable FindAll(this string s, string pattern, bool isCompiled = false, bool ignoreCase = false)
    {
        if (s == null || pattern == null) return new List();

        RegexOptions options = RegexOptions.CultureInvariant;
        if (ignoreCase)
            options |= RegexOptions.IgnoreCase;
        if (isCompiled)
            options |= RegexOptions.Compiled;
        
        return Regex.Matches(s, pattern, options).Select(m => m.Value);
    }

    public static Match Match(this string s, string pattern, bool isCompiled = false, bool ignoreCase = false)
    {
        if (s == null || pattern == null) return UnsuccessfulMatch;

        RegexOptions options = RegexOptions.CultureInvariant;
        if (ignoreCase)
            options |= RegexOptions.IgnoreCase;
        if (isCompiled)
            options |= RegexOptions.Compiled;

        return Regex.Match(s, pattern, options);
    }

    public static Match MatchLast(this string s, string pattern, bool isCompiled = false, bool ignoreCase = false)
    {
        if (s == null || pattern == null) return UnsuccessfulMatch;

        RegexOptions options = RegexOptions.CultureInvariant | RegexOptions.RightToLeft;
        if (ignoreCase)
            options |= RegexOptions.IgnoreCase;
        if (isCompiled)
            options |= RegexOptions.Compiled;

        return Regex.Match(s, pattern, options);
    }

    public static string Find(this string s, string pattern, bool isCompiled = false, bool ignoreCase = false)
    {
        if (s == null || pattern == null) return "";

        RegexOptions options = RegexOptions.CultureInvariant;
        if (ignoreCase)
            options |= RegexOptions.IgnoreCase;
        if (isCompiled)
            options |= RegexOptions.Compiled;

        Match match = Regex.Match(s, pattern, options);
        return match.Success ? match.Value : "";
    }

    public static string FindLast(this string s, string pattern, bool isCompiled = false, bool ignoreCase = false)
    {
        if (s == null || pattern == null) return "";

        RegexOptions options = RegexOptions.CultureInvariant | RegexOptions.RightToLeft;
        if (ignoreCase)
            options |= RegexOptions.IgnoreCase;
        if (isCompiled)
            options |= RegexOptions.Compiled;
        Match match = Regex.Match(s, pattern, options);
        return match.Success ? match.Value : "";
    }

}

The MatchCollection class itself is kind of an odd duck, as it’s a useful collection that was never fully updated for use with Linq. If one makes frequent use of regular expressions it might be useful to also put a few simple Linq-enabling extension methods on MatchCollection itself; it’s up to the user whether to make them global. I leave it to the reader to implement the rest of the Linq methods as deemed useful in a particular case.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

public static partial class MatchCollectionExtensions
{
    public static bool Any(this MatchCollection source, Func<Match, bool> predicate)
    {
        return source.Cast<Match>().Any(predicate);
    }

    public static Match First(this MatchCollection source)
    {
        return (source == null || source.Count == 0) ? null : source[0];
    }

    public static Match First(this MatchCollection source, Func<Match, bool> predicate) {
        return source.Cast<Match>().First(predicate);
    }

    public static Match FirstOrDefault(this MatchCollection source)
    {
        return (source == null || source.Count == 0) ? null : source[0];
    }

    public static Match FirstOrDefault<T>(this MatchCollection source, Func<Match, bool> predicate)
    {
        return source.Cast<Match>().FirstOrDefault(predicate);
    }

    public static Match Last<T>(this MatchCollection source)
    {
        return (source == null || source.Count == 0) ? null : source[0];
    }

    public static Match Last(this MatchCollection source, Func<Match, bool> predicate)
    {
        return source.Cast<Match>().Last(predicate);
    }

    public static Match LastOrDefault(this MatchCollection source)
    {
        return (source == null || source.Count == 0) ? null : source[source.Count - 1];
    }

    public static Match LastOrDefault<T>(this MatchCollection source, Func<Match, bool> predicate)
    {
        return source.Cast<Match>().LastOrDefault(predicate);
    }

    public static IOrderedEnumerable<Match> OrderBy<Match, TKey>(this MatchCollection source, Func<Match, TKey> keySelector)
    {
        return source.Cast<Match>().OrderBy(keySelector);
    }

    public static IOrderedEnumerable<Match> OrderBy<Match, TKey>(this MatchCollection source, Func<Match, TKey> keySelector, IComparer<TKey> comparer)
    {
        return source.Cast<Match>().OrderBy(keySelector, comparer);
    }

    public static IOrderedEnumerable<Match> OrderByDescending<Match, TKey>(this MatchCollection source, Func<Match, TKey> keySelector)
    {
        return source.Cast<Match>().OrderByDescending(keySelector);
    }

    public static IOrderedEnumerable<Match> OrderByDescending<Match, TKey>(this MatchCollection source, Func<Match, TKey> keySelector, IComparer<TKey> comparer)
    {
        return source.Cast<Match>().OrderByDescending(keySelector, comparer);
    }

    public static IEnumerable<Match> Reverse(this MatchCollection source)
    {
        return source.Cast<Match>().Reverse();
    }

    public static IEnumerable<TResult> Select<TResult>(this MatchCollection source, Func<Match, TResult> selector)
    {
        return source.Cast<Match>().Select(selector);
    }

    public static IEnumerable<TResult> Select<TResult>(this MatchCollection source, Func<Match, int, TResult> selector)
    {
        return source.Cast<Match>().Select(selector);
    }

    public static IEnumerable<Match> Skip(this MatchCollection source, int count)
    {
        return source.Cast<Match>().Skip(count);
    }

    public static IEnumerable<Match> Take(this MatchCollection source, int count)
    {
        return source.Cast<Match>().Take(count);
    }

    public static Dictionary<TKey, Match> ToDictionary<Match, TKey>(this MatchCollection source, Func<Match, TKey> keySelector)
    {
        return source.Cast<Match>().ToDictionary(keySelector);
    }

    public static Dictionary<TKey, Match> ToDictionary<Match, TKey>(this MatchCollection source, Func<Match, TKey> keySelector, IEqualityComparer<TKey> comparer)
    {
        return source.Cast<Match>().ToDictionary(keySelector, comparer);
    }

    public static Dictionary<TKey, TElement> ToDictionary<Match, TKey, TElement>(this MatchCollection source, Func<Match, TKey> keySelector, Func<Match, TElement> elementSelector)
    {
        return source.Cast<Match>().ToDictionary(keySelector, elementSelector);
    }

    public static Dictionary<TKey, TElement> ToDictionary<Match, TKey, TElement>(this MatchCollection source, Func<Match, TKey> keySelector, Func<Match, TElement> elementSelector, IEqualityComparer<TKey> comparer)
    {
        return source.Cast<Match>().ToDictionary(keySelector, elementSelector, comparer);
    }

    public static List<Match> ToList<T>(this MatchCollection source)
    {
        return source.Cast<Match>().ToList();
    }

    public static IEnumerable<Match> Where<T>(this MatchCollection source, Func<Match, bool> predicate)
    {
        return source.Cast<Match>().Where(predicate);
    }
}

The MemoryCache class is another fundamentally useful .NET class that has a bit of a clumsy API, which can be remedied by providing facades to the clunkier methods. Very often one merely wants to set a cache duration when setting an object, and get an object of a specified type safely.

using System;
using System.Runtime.Caching;

public static partial class MemoryCacheExtensions
{
    public static void Set(this MemoryCache cache, string key, object value, long durationMilliseconds)
    {
        if (cache == null || key == null) throw new ArgumentNullException();
        else if (durationMilliseconds <= 0) return;
        else if (value == null)
            try { cache.Remove(key); } catch {}

        DateTimeOffset expiration = DateTime.Now.AddMilliseconds(durationMilliseconds);
        cache.Set(key, value, expiration);
    }

    public static T Get<T>(this MemoryCache cache, string key, T defaultValue = default(T))
    {
        object value = cache.Get(key);

        if (value == null || !(value is T))
            return defaultValue;
        else
            return (T)value;
    }
}

The common thread in these examples is that even in the .NET FCL (and CoreFX for that matter), API design is not always optimal. A primary use case for extension methods is where a developer desires to add useful functionality to an API, but without the ability to control the API itself, and this applies to .NET fundamentals as easily as anything else. When one is adding those methods to a core class, it may make sense to make the extension methods themselves as visible as the core classes so extended–and here the efficiency/usability gains of broadening visibility can be tainting in a sense, as above where the MatchCollection extensions are made global by virtue of the fact they may be returned from the result of another global extension method on the String class.

This may not be everyone’s cup of tea, and in fact I don’t tend to do this for seldom-used, specialty classes ever. The String class is obviously a different case from even MatchCollection and the like. And whenever creating extension methods, but increasing with their visibility, one should strongly consider placing them in partial, wisely named classes to avoid naming collisions in the future.

Leave a comment