Reputation: 422
As a hobby project (and to immerse myself more deeply in generics/extension methods), I'm writing a parameter checking library!
I have a model called Argument that describes a parameter and looks like this:
public class Argument<T>
{
internal Argument(string name, T value)
{
Name = name;
Value = value;
}
public string Name { get; private set; }
public T Value { get; private set; }
}
When validation for a parameter begins, an instance of this object is created, and individual validations are performed by invoking extension methods (that contain the actual logic) that hang off of it.
One such extension method verifies that a collection contains at least one item, and currently looks like this:
public static Argument<IEnumerable<T>> HasItems<T>(this Argument<IEnumerable<T>> argument)
{
if (!argument.Value.Any())
throw Error.Generic(argument.Name, "Collection contains no items.");
return argument;
}
But it doesn't appear to work. If I were, say, to write this unit test:
[TestMethod]
public void TestMethod1()
{
var argument = new List<int>() { 1, 2, 6, 3, -1, 5, 0 };
Validate.Argument("argument", argument)
.IsNotNull()
.HasItems()
.All(v => v.IsGreaterThan(0));
}
HasItems doesn't show up in Intellisense, and I get this compile error:
'Validation.Argument<System.Collections.Generic.List<int>>'
does not contain a definition for 'HasItems' and no extension method 'HasItems' accepting a first argument of type'Validation.Argument<System.Collections.Generic.List<int>>'
could be found (are you missing a using directive or an assembly reference?)
And if I try passing the value directly into the extension method, like so:
CollectionTypeExtensions.HasItems(Validate.Argument("argument", argument));
I get this:
The best overloaded method match for
'Validation.CollectionTypeExtensions.HasItems<int>(Validation.Argument<System.Collections.Generic.IEnumerable<int>>)'
has some invalid arguments
Based on my research, what I'd need for this to work is called "variance," and applies to interfaces and delegates, but not to classes (ie: all classes are invariant.)
That said, it might work another way. One that comes to mind is to rewrite it to go straight to T, like so:
public static Argument<T> HasItems<T, TElement>(this Argument<T> argument)
where T : IEnumerable<TElement>
{
if (!argument.Value.Any())
throw Error.Generic(argument.Name, "Collection contains no items.");
return argument;
}
..But that doesn't work either because it requires TElement to be specified explicitly when the method is invoked. I could also fall back to using the non-generic IEnumerable interface in the type constraint, but then I'd have to either find a way to coerce the IEnumerable into IEnumerable (which would require knowing what T is in that context), or duplicating the functionality of Any() in order to test for the existence of any items, and there's another extension method (All) that that would be very, very messy for, so I'd rather avoid it.
So ultimately, I guess my question is: how do I get my extension method to attach properly?
Upvotes: 6
Views: 932
Reputation: 2596
I believe what you actually want is an interface IArgument
which is covariant on T
:
public static class Validate
{
public static IArgument<T> Argument<T>(string name, T value)
{
return new Argument<T>(name, value);
}
}
public interface IArgument<out T>
{
string Name { get; }
T Value { get; }
}
public class Argument<T> : IArgument<T>
{
internal Argument(string name, T value)
{
Name = name;
Value = value;
}
public string Name { get; private set; }
public T Value { get; private set; }
}
public static class ExtensionMethods
{
public static IArgument<T> IsNotNull<T>(this IArgument<T> argument)
{
return argument;
}
public static IArgument<IEnumerable<T>> HasItems<T>(this IArgument<IEnumerable<T>> argument)
{
return argument;
}
public static IArgument<IEnumerable<T>> All<T>(this IArgument<IEnumerable<T>> argument, Predicate<T> predicate)
{
return argument;
}
}
[TestMethod]
public void TestMethod1()
{
List<int> argument = new List<int>() { 1, 2, 6, 3, -1, 5, 0 };
Validate.Argument("argument", argument)
.IsNotNull()
.HasItems()
.All(v => v > 0);
}
Upvotes: 0
Reputation: 109567
Does this work for you? Seems a bit cludgy, but it does actually work.
public static Argument<T> HasItems<T>(this Argument<T> argument) where T: IEnumerable
{
if (!argument.Value.Cast<object>().Any())
{
throw Error.Generic(argument.Name, "Collection contains no items.");
}
return argument;
}
Upvotes: 2