The curious case of the unsolved extension methods

Edit this page | 3 minute read

As part of my effort to improve the type-safety of Fluent Assertions, I’ve been investigating the possibility to use C# extension methods all the way. Unfortunately I think I’ve ran into the limitations of C# 3.0 (and C# 4.0 since it doesn’t add anything useful for this). Essentially, I’d like to do the following things.  

var someList = new List<string> {"hello world"};

someList.Should().HaveCount(1);

someList.Should().Contain(s => s.Length > 0);

The first one is easy because it does not require any type-specific extensions and can be implemented by the following piece of code:

static class Extensions

{

    public static EnumerableConstraints Should(this IEnumerable subject)

    {

        return new EnumerableConstraints();

    }

}

public class EnumerableConstraints

{

    public ShouldConstraint(IEnumerable subject) { }

 

    public void HaveCount(int expectedCount)  { }

}

The second one is more tricky. In order for the Contain() method to take a lambda expression on a List<string>, it needs to know the actual type of items in the IEnumerable collection. We can fix that by changing the type of the subject parameter to IEnumerable<T> and pass the <T> type parameter to the class that exposes the actual assertion methods:

static class Extensions

{

    public static EnumerableConstraints<T> Should<T>(this IEnumerable<T> subject)

    {

        return new EnumerableConstraints<T>(subject);

    }
}

 

public class EnumerableConstraints<T>

{

    public void HaveCount(int expectedCount) { }

    public void Contain(Func<T, bool> predicate) { }

}

That solves our initial two requirements, so let’s add another one. I’d like to be able to use a lambda expression on any possible type. Something like this:

var dto = new FindOrdersDto { CustomerName = “blah”, MaxItems = 10 };
dto.Should().Match(d => d.MaxItems < 100);

The first idea that pops up in my mind is to add an overload of Should<T> to the Extensions class, like this:

static class Extensions

{

    public static EnumerableConstraints<T> Should<T>(this IEnumerable<T> subject) { }

 

    public static BasicConstraints Should<T>(this T subject) { }

}

Unfortunately, this overload takes precedence over the overload taking an IEnumerable<T> and breaks the first working example. That’s a real bummer. If only I could use a where constraint stating that the <T> does not  comply with a specific constraint. For instance

public static BasicConstraints Should<T>(this T subject) where not T : IEnumerable

I’ve also tried to change the HaveCount(), Contain() and Match() methods into extension methods on a single generic class ShouldConstraints<T> like this:

static class Extensions

{

    public static ShouldConstraints<T> Should<T>(this T subject)

    {

        return new ShouldConstraints<T>(subject);

    }

 

    public static ShouldConstraints<T> HaveCount<T, TItem>(

        this ShouldConstraints<T> constraints, int expectedCount) where T : IEnumerable<TItem> { }

 

    public static ShouldConstraints<T> Contain<T, TItem>(

        this ShouldConstraints<T> constraints, Func<TItem, bool> predicate) { }

 

    public static ShouldConstraints<T> Match<T, TItem>(

        this ShouldConstraints<T> constraints, Func<TItem, bool> predicate) { }

}

 

class ShouldConstraints<T>

{

    public ShouldConstraints(T subject)

    {

        Subject = subject;

    }

 

    protected T Subject { get; set; }

}

This compiles fine, but causes the compiler to complain about missing type parameters because it is unable to infer these from the usage. This forces us to use this rather ugly syntax:

var someList = new List<string> {"hello world"};

someList.Should().HaveCount<List<string>, string>(1);

someList.Should().Contain<List<string>, string>(s => s.Length > 0);

Other solutions that crossed my mind included testing the ability to define two extension methods with the same name and the same parameters but having different constraints such as:

public static EnumerableConstraints Should<T>(this T subject) where T : IEnumerable
public static ComparableConstraints Should<T>(this T subject) where T : IComparable

But this is, as I expected, not supported by C#.

The conclusion? Well, as far as I can tell, my problem is currently unsolvable…

Leave a Comment