20

I have an interface similar to the one below:

public interface IInterface<T>
    where T : IInterface<T>
{
}

And now I need to create a type representing this interface using reflection, e.g.

typeof(IInterface<>).MakeGenericType(someType);

However, I don't actually know what type 'someType' will be until runtime, and it's possible that the type won't be valid as a type argument for the generic interface, so MakeGenericType fails.

The question is, how can I check that 'someType' is valid for the generic constraint?

Simon
  • 5,373
  • 1
  • 34
  • 46

4 Answers4

27

To be honest, the simplest approach would be to just call MakeGenericType and catch the ArgumentException that will be thrown if any type argument is wrong (or if you've got the wrong number of type parameters).

While you could use Type.GetGenericParameterConstraints to find the constraints and then work out what each of them means, it's going to be ugly and bug-prone code.

I don't usually like suggesting "just try it and catch" but in this case I think it's going to be the most reliable approach. Otherwise you're just reimplementing the checks that the CLR is going to perform anyway - and what are the chances you'll reimplement them perfectly? :)

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Fair enough, thanks Jon. Was hoping to avoid that but as performance isn't an issue in this code, if you say that's the easiest way it's good enough for me! – Simon Feb 01 '11 at 15:41
  • Isn't this sort of the "lazy man's" approach? I was thumped on the skull by many elder statesmen in my early years for using exceptions to check things rather than check things to avoid an exception. – Joel Etherton Feb 01 '11 at 15:42
  • If you can improve on Jon's answer I'll be more than happy Joel :-) – Simon Feb 01 '11 at 15:43
  • 1
    @Jon Skeet: You really think it's that ugly and bug prone? I just implemented something like this last week, and I don't find it ugly and I think it's not buggy (at least, it passes all my tests). I have it working to check for class, non-nullable value type, and default constructor constraints, as well as type hierarchy constraints. Also, I can't believe you're suggesting using exceptions for flow control after just agreeing yesterday that it's nasty, Nasty, NASTY. For shame. – jason Feb 01 '11 at 15:45
  • @Jason: Yes, I believe it's going to be ugly and bug prone if you want to implement the general case. Making your code pass all *your* tests isn't the tricky bit, because that only shows you can handle the situations you've thought of. In this case, it's convincing yourself that you've thought of all the possible situations that's the tricky bit. It's also redundant, of course: the real source of truth is what the CLR is going to allow. While this approach has a certain smell to it and won't perform well if you have many cases of invalid type arguments, in practical terms I think it's okay. – Jon Skeet Feb 01 '11 at 15:49
  • 6
    @Joel: Yes, this is the lazy man's approach, and *usually* using exceptions is the wrong way of checking things. However, in this case we're trying to check something which can get pretty complex, and that check is already coded up elsewhere. If the framework had a TryMakeGenericType, that would be great... – Jon Skeet Feb 01 '11 at 15:50
  • +1 for TryMakeGenericType! "and what are the chances you'll reimplement them perfectly? :)" -> pretty low I'd guess! However, it only needs to work for the one situation I posted originally (for now, anyway) – Simon Feb 01 '11 at 15:54
  • @Jon Skeet: That's always true for unit testing. But there really aren't that many cases (`class`, `struct`, and `new()`, a base type and some interfaces and various combinations of the each) and so it's fairly easy to have a high level of confidence that one's implementation is correct. That's one of the points of unit testing anyway. – jason Feb 01 '11 at 16:02
  • 1
    @Jason: By the time you've got "where T1 : T2" and "where T1 : IFoo" and the like, I think it could get quite complicated. If you're willing to send me your code, I'd be happy to try to break it :) More importantly, it's *still* effectively replicating code which already exists; it's *still* code you need to maintain; it's *still* code you don't need to write if you're willing to accept the slight performance hit and ugliness of just catching the exception. – Jon Skeet Feb 01 '11 at 16:04
  • @Simon - uh, no I can't improve on his answer. His answer just violated some rules that one my programming coaches used to pound into me. That and nesting loops. – Joel Etherton Feb 01 '11 at 16:37
  • 2
    @Joel: The important thing about rules is knowing why they're *usually* applicable, and when it's okay to break them :) – Jon Skeet Feb 01 '11 at 16:38
  • @Jon Skeet - righty-o. It makes sense if catching the exception is less expensive and more reliable anyway. Maybe someone with lots of spare time can craft up your `TryMakeGenericType` as an extension method :) – Joel Etherton Feb 01 '11 at 16:39
  • @Joel: I agree about violating a rule, but as Jon points out in this case I think it's acceptable. I'll look forward to your TryMakeGenericType extension method! :-) – Simon Feb 01 '11 at 17:38
  • @Simon, @Jon Skeet - You caught me on a boring day, and a little searching provided me with a link that had an enterprising solution that seems to work marvelously (though I haven't tested it robustly). I'll post an answer here just to forward it along. – Joel Etherton Feb 01 '11 at 18:03
  • @Jon Skeet: I'll probably take you up on your offer to look at my code. I'm sure you'll break it, but I know I'll learn something in the process. Thanks for the offer, I'll reach out to you privately sometime next week. – jason Feb 03 '11 at 05:40
  • This might be a totally stupid comment that reveals my ignorance about CLR internals, but does `MakeGenericType` have nasty side-effects? Is there a possibility that this will have an unrecoverable (until AppDomain unload) memory cost (e.g. the IL for the constructed type gets immediately JITted)? If I had a fancy search process that tried to match hundreds of types as candidate args to hundreds of generic types (e.g. output: Types A,B are valid type args for generic type C), would this become a problem? If so, this technique would be out and we would have to implement the logic ourselves? – Ani Feb 07 '11 at 12:05
  • @Ani: I strongly suspect that it does indeed use up memory. I don't believe that it will JIT the whole type on the desktop CLR, but I'd still expect a certain amount of memory to be used. You'd want to test it to find out how much :) – Jon Skeet Feb 07 '11 at 12:48
  • If that's true, that weakens this answer considerably IMO. And also raises the question of why on earth there's no `CanMakeGenericType`. – Ani Feb 07 '11 at 12:54
  • 1
    @Ani: It weakens the answer in the specific case where you're using trying to use it for hundreds or thousands of types - although the memory cost may well still be small enough to be okay - but I suspect that in *most* cases where you want to know this, it's actually fine. – Jon Skeet Feb 07 '11 at 12:58
  • Fair enough, but it's a bit like answering "How do I check if a string has 10 characters?" with `string.Intern(myString).Length == 10`. Of course, I appreciate that hundreds or thousands of *types* is different from hundreds or thousands of *strings*, but my point is that leaking memory simply by asking a *question* isn't a good idea. – Ani Feb 07 '11 at 13:04
  • @Ani: The difference is that with the `string.Intern` answer there's an obviously better approach - just use `string.Length`. If there's an obviously better approach here, I'm all ears :) – Jon Skeet Feb 07 '11 at 13:11
  • I added an answer with an implementation of a method that doesn't rely on exceptions. I'd be curious to have your opinion on it, @JonSkeet. – Lazlo Jan 15 '17 at 22:05
  • @Lazlo: I'm afraid I don't have time to do a 370 line code review at the moment, but it does seem error-prone - any time you're trying to do the same kind of complicated checking as something else, there's a high chance of the implementations diverging. I'd still rather trust a single implementation, personally. – Jon Skeet Jan 16 '17 at 07:31
5

This is possible. Given a constraint, you use Type.GenericParameterAttributes and the masks

GenericParameterAttributes.ReferenceTypeConstraint
GenericParameterAttributes.NotNullableValueTypeConstraint
GenericParameterAttributes.DefaultConstructorConstraint

to check for the presence of class, struct or new() constraints. You can easily check if a given type satisfies these constraints (the first is easy to implement (use Type.IsClass), the second is slightly tricky but you can do it using reflection, and the third has a little gotcha that your unit testing will detect (Type.GetConstructor(new Type[0]) doesn't return the default constructor for value types but you know those have a default constructor anyway).

After this, you use Type.GetGenericParameterConstraints to get the type hierarchy constraints (the where T : Base, IInterface like constraints) and run through them to check that the given type satisfies them.

jason
  • 236,483
  • 35
  • 423
  • 525
  • Thanks for the answer Jason - I'm taking the easy way out and just catching the exception... the code runs once on startup so in this case I'm bending the usual rules. – Simon Feb 01 '11 at 17:39
  • Small correction needed here. It is incorrect when you say "the first is easy to implement (use `Type.IsClass`)". The `class` constraint doesn't constrain the type to a class. It actually constrains it to any reference type (including interfaces etc.) See https://msdn.microsoft.com/en-us/library/d5x73970.aspx. I have no idea why Microsoft decided to use the `class` keyword for this constraint as it's very confusing. You need to use `sometype.IsValueType == false` instead. – 0b101010 Apr 20 '15 at 13:50
3

Here's my implementation of 3 extension methods:

  • bool CanMakeGenericTypeVia(this Type openConstructedType, Type closedConstructedType)
  • Type MakeGenericTypeVia(this Type openConstructedType, Type closedConstructedType)
  • MethodInfo MakeGenericMethodVia(this MethodInfo openConstructedMethod, params Type[] closedConstructedParameterTypes)

The first allows you to check if a closed-constructed type matches an open-constructed type definition. If so, the second can infer all the required type arguments to return a closed-constructed from a given closed-constructed type. Finally, the third method can resolve all this automatically for methods.

Note that these methods will not fail or return false if you pass another open-constructed type as the "closed-constructed" type argument, as long as this second type respects all the type constraints of the initial open-constructed type. They will instead resolve as much type information as possible from the given types. Therefore, if you want to make sure the resolution gave a fully closed-constructed type, you should check that the result's ContainsGenericParameters returns false. This matches the behaviour of .NET's MakeGenericType or MakeGenericMethod.

Also note that I'm not very well informed on co- and contravariance, so these implementations might not be correct in that regard.

Example usage:

public static void GenericMethod<T0, T1>(T0 direct, IEnumerable<T1> generic)
     where T0 : struct
     where T1 : class, new(), IInterface
{ }

public interface IInterface { }
public class CandidateA : IInterface { private CandidateA(); }
public struct CandidateB : IInterface { }
public class CandidateC { public CandidateC(); }
public class CandidateD : IInterface { public CandidateD(); }

var method = GetMethod("GenericMethod");
var type0 = method.GetParameters()[0].ParameterType;
var type1 = method.GetParameters()[1].ParameterType;

// Results:

type0.CanMakeGenericTypeVia(typeof(int)) // true
type0.CanMakeGenericTypeVia(typeof(IList)) // false, fails struct

type1.CanMakeGenericTypeVia(typeof(IEnumerable<CandidateA>)) 
// false, fails new()

type1.CanMakeGenericTypeVia(typeof(IEnumerable<CandidateB>)) 
// false, fails class

type1.CanMakeGenericTypeVia(typeof(IEnumerable<CandidateC>)) 
// false, fails : IInterface

type1.CanMakeGenericTypeVia(typeof(IEnumerable<CandidateD>)) 
// true

type0.MakeGenericTypeVia(typeof(int)) 
// typeof(int)

type1.MakeGenericTypeVia(typeof(List<CandidateD>)) 
// IEnumerable<CandidateD>

method.MakeGenericMethodVia(123.GetType(), (new CandidateD[0]).GetType()) 
// GenericMethod(int, IEnumerable<CandidateD>)

method.MakeGenericMethodVia(123.GetType(), type1)
// GenericMethod<T1>(int, IEnumerable<T1>)
// (partial resolution)

Implementation:

public static bool CanMakeGenericTypeVia(this Type openConstructedType, Type closedConstructedType)
{
    if (openConstructedType == null)
    {
        throw new ArgumentNullException("openConstructedType");
    }

    if (closedConstructedType == null)
    {
        throw new ArgumentNullException("closedConstructedType");
    }

    if (openConstructedType.IsGenericParameter) // e.g.: T
    {
        // The open-constructed type is a generic parameter. 

        // First, check if all special attribute constraints are respected.

        var constraintAttributes = openConstructedType.GenericParameterAttributes;

        if (constraintAttributes != GenericParameterAttributes.None)
        {
            // e.g.: where T : struct
            if (constraintAttributes.HasFlag(GenericParameterAttributes.NotNullableValueTypeConstraint) &&
                !closedConstructedType.IsValueType)
            {
                return false;
            }

            // e.g.: where T : class
            if (constraintAttributes.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint) &&
                closedConstructedType.IsValueType)
            {
                return false;
            }

            // e.g.: where T : new()
            if (constraintAttributes.HasFlag(GenericParameterAttributes.DefaultConstructorConstraint) &&
                closedConstructedType.GetConstructor(Type.EmptyTypes) == null)
            {
                return false;
            }

            // TODO: Covariance and contravariance?
        }

        // Then, check if all type constraints are respected.

        // e.g.: where T : BaseType, IInterface1, IInterface2
        foreach (var constraint in openConstructedType.GetGenericParameterConstraints())
        {
            if (!constraint.IsAssignableFrom(closedConstructedType))
            {
                return false;
            }
        }

        return true;
    }
    else if (openConstructedType.ContainsGenericParameters)
    {
        // The open-constructed type is not a generic parameter but contains generic parameters.
        // It could be either a generic type or an array.

        if (openConstructedType.IsGenericType) // e.g. Generic<T1, int, T2>
        {
            // The open-constructed type is a generic type.

            var openConstructedGenericDefinition = openConstructedType.GetGenericTypeDefinition(); // e.g.: Generic<,,>
            var openConstructedGenericArguments = openConstructedType.GetGenericArguments(); // e.g.: { T1, int, T2 }

            // Check a list of possible candidate closed-constructed types:
            //  - the closed-constructed type itself
            //  - its base type, if any (i.e.: if the closed-constructed type is not object)
            //  - its implemented interfaces

            var inheritedClosedConstructedTypes = new List<Type>();

            inheritedClosedConstructedTypes.Add(closedConstructedType);

            if (closedConstructedType.BaseType != null)
            {
                inheritedClosedConstructedTypes.Add(closedConstructedType.BaseType);
            }

            inheritedClosedConstructedTypes.AddRange(closedConstructedType.GetInterfaces());

            foreach (var inheritedClosedConstructedType in inheritedClosedConstructedTypes)
            {
                if (inheritedClosedConstructedType.IsGenericType && 
                    inheritedClosedConstructedType.GetGenericTypeDefinition() == openConstructedGenericDefinition)
                {
                    // The inherited closed-constructed type and the open-constructed type share the same generic definition.

                    var inheritedClosedConstructedGenericArguments = inheritedClosedConstructedType.GetGenericArguments(); // e.g.: { float, int, string }

                    // For each open-constructed generic argument, recursively check if it
                    // can be made into a closed-constructed type via the closed-constructed generic argument.

                    for (int i = 0; i < openConstructedGenericArguments.Length; i++)
                    {
                        if (!openConstructedGenericArguments[i].CanMakeGenericTypeVia(inheritedClosedConstructedGenericArguments[i])) // !T1.IsAssignableFromGeneric(float)
                        {
                            return false;
                        }
                    }

                    // The inherited closed-constructed type matches the generic definition of 
                    // the open-constructed type and each of its type arguments are assignable to each equivalent type
                    // argument of the constraint.

                    return true;
                }
            }

            // The open-constructed type contains generic parameters, but no
            // inherited closed-constructed type has a matching generic definition.

            return false;
        }
        else if (openConstructedType.IsArray) // e.g. T[]
        {
            // The open-constructed type is an array.

            if (!closedConstructedType.IsArray ||
                closedConstructedType.GetArrayRank() != openConstructedType.GetArrayRank())
            {
                // Fail if the closed-constructed type isn't an array of the same rank.
                return false;
            }

            var openConstructedElementType = openConstructedType.GetElementType();
            var closedConstructedElementType = closedConstructedType.GetElementType();

            return openConstructedElementType.CanMakeGenericTypeVia(closedConstructedElementType);
        }
        else
        {
            // I don't believe this can ever happen.

            throw new NotImplementedException("Open-constructed type contains generic parameters, but is neither an array nor a generic type.");
        }
    }
    else
    {
        // The open-constructed type does not contain generic parameters,
        // we can proceed to a regular closed-type check.

        return openConstructedType.IsAssignableFrom(closedConstructedType);
    }
}

public static Type MakeGenericTypeVia(this Type openConstructedType, Type closedConstructedType, Dictionary<Type, Type> resolvedGenericParameters, bool safe = true)
{
    if (openConstructedType == null)
    {
        throw new ArgumentNullException("openConstructedType");
    }

    if (closedConstructedType == null)
    {
        throw new ArgumentNullException("closedConstructedType");
    }

    if (resolvedGenericParameters == null)
    {
        throw new ArgumentNullException("resolvedGenericParameters");
    }

    if (safe && !openConstructedType.CanMakeGenericTypeVia(closedConstructedType))
    {
        throw new InvalidOperationException("Open-constructed type is not assignable from closed-constructed type.");
    }

    if (openConstructedType.IsGenericParameter) // e.g.: T
    {
        // The open-constructed type is a generic parameter.
        // We can directly map it to the closed-constructed type.

        // Because this is the lowest possible level of type resolution,
        // we will add this entry to our list of resolved generic parameters
        // in case we need it later (e.g. for resolving generic methods).

        // Note that we allow an open-constructed type to "make" another
        // open-constructed type, as long as the former respects all of 
        // the latter's constraints. Therefore, we will only add the resolved 
        // parameter to our dictionary if it actually is resolved.

        if (!closedConstructedType.ContainsGenericParameters)
        {
            if (resolvedGenericParameters.ContainsKey(openConstructedType))
            {
                if (resolvedGenericParameters[openConstructedType] != closedConstructedType)
                {
                    throw new InvalidOperationException("Nested generic parameters resolve to different values.");
                }
            }
            else
            {
                resolvedGenericParameters.Add(openConstructedType, closedConstructedType);
            }
        }

        return closedConstructedType;
    }
    else if (openConstructedType.ContainsGenericParameters) // e.g.: Generic<T1, int, T2>
    {
        // The open-constructed type is not a generic parameter but contains generic parameters.
        // It could be either a generic type or an array.

        if (openConstructedType.IsGenericType) // e.g. Generic<T1, int, T2>
        {
            // The open-constructed type is a generic type.

            var openConstructedGenericDefinition = openConstructedType.GetGenericTypeDefinition(); // e.g.: Generic<,,>
            var openConstructedGenericArguments = openConstructedType.GetGenericArguments(); // e.g.: { T1, int, T2 }

            // Check a list of possible candidate closed-constructed types:
            //  - the closed-constructed type itself
            //  - its base type, if any (i.e.: if the closed-constructed type is not object)
            //  - its implemented interfaces

            var inheritedCloseConstructedTypes = new List<Type>();

            inheritedCloseConstructedTypes.Add(closedConstructedType);

            if (closedConstructedType.BaseType != null)
            {
                inheritedCloseConstructedTypes.Add(closedConstructedType.BaseType);
            }

            inheritedCloseConstructedTypes.AddRange(closedConstructedType.GetInterfaces());

            foreach (var inheritedCloseConstructedType in inheritedCloseConstructedTypes)
            {
                if (inheritedCloseConstructedType.IsGenericType && 
                    inheritedCloseConstructedType.GetGenericTypeDefinition() == openConstructedGenericDefinition)
                {
                    // The inherited closed-constructed type and the open-constructed type share the same generic definition.

                    var inheritedClosedConstructedGenericArguments = inheritedCloseConstructedType.GetGenericArguments(); // e.g.: { float, int, string }

                    // For each inherited open-constructed type generic argument, recursively resolve it
                    // via the equivalent closed-constructed type generic argument.

                    var closedConstructedGenericArguments = new Type[openConstructedGenericArguments.Length];

                    for (int j = 0; j < openConstructedGenericArguments.Length; j++)
                    {
                        closedConstructedGenericArguments[j] = MakeGenericTypeVia
                        (
                            openConstructedGenericArguments[j], 
                            inheritedClosedConstructedGenericArguments[j],
                            resolvedGenericParameters,
                            safe: false // We recursively checked before, no need to do it again
                        );

                        // e.g.: Resolve(T1, float)
                    }

                    // Construct the final closed-constructed type from the resolved arguments

                    return openConstructedGenericDefinition.MakeGenericType(closedConstructedGenericArguments);
                }
            }

            // The open-constructed type contains generic parameters, but no 
            // inherited closed-constructed type has a matching generic definition.
            // This cannot happen in safe mode, but could in unsafe mode.

            throw new InvalidOperationException("Open-constructed type is not assignable from closed-constructed type.");
        }
        else if (openConstructedType.IsArray) // e.g. T[]
        {
            var arrayRank = openConstructedType.GetArrayRank();

            // The open-constructed type is an array.

            if (!closedConstructedType.IsArray || 
                closedConstructedType.GetArrayRank() != arrayRank)
            {
                // Fail if the closed-constructed type isn't an array of the same rank.
                // This cannot happen in safe mode, but could in unsafe mode.
                throw new InvalidOperationException("Open-constructed type is not assignable from closed-constructed type.");
            }

            var openConstructedElementType = openConstructedType.GetElementType();
            var closedConstructedElementType = closedConstructedType.GetElementType();

            return openConstructedElementType.MakeGenericTypeVia
            (
                closedConstructedElementType, 
                resolvedGenericParameters,
                safe: false
            ).MakeArrayType(arrayRank);
        }
        else
        {
            // I don't believe this can ever happen.

            throw new NotImplementedException("Open-constructed type contains generic parameters, but is neither an array nor a generic type.");
        }
    }
    else
    {
        // The open-constructed type does not contain generic parameters,
        // it is by definition already resolved.

        return openConstructedType;
    }
}

public static MethodInfo MakeGenericMethodVia(this MethodInfo openConstructedMethod, params Type[] closedConstructedParameterTypes)
{
    if (openConstructedMethod == null)
    {
        throw new ArgumentNullException("openConstructedMethod");
    }

    if (closedConstructedParameterTypes == null)
    {
        throw new ArgumentNullException("closedConstructedParameterTypes");
    }

    if (!openConstructedMethod.ContainsGenericParameters)
    {
        // The method contains no generic parameters,
        // it is by definition already resolved.
        return openConstructedMethod;
    }

    var openConstructedParameterTypes = openConstructedMethod.GetParameters().Select(p => p.ParameterType).ToArray();

    if (openConstructedParameterTypes.Length != closedConstructedParameterTypes.Length)
    {
        throw new ArgumentOutOfRangeException("closedConstructedParameterTypes");
    }

    var resolvedGenericParameters = new Dictionary<Type, Type>();

    for (int i = 0; i < openConstructedParameterTypes.Length; i++)
    {
        // Resolve each open-constructed parameter type via the equivalent
        // closed-constructed parameter type.

        var openConstructedParameterType = openConstructedParameterTypes[i];
        var closedConstructedParameterType = closedConstructedParameterTypes[i];

        openConstructedParameterType.MakeGenericTypeVia(closedConstructedParameterType, resolvedGenericParameters);
    }

    // Construct the final closed-constructed method from the resolved arguments

    var openConstructedGenericArguments = openConstructedMethod.GetGenericArguments();
    var closedConstructedGenericArguments = openConstructedGenericArguments.Select(openConstructedGenericArgument => 
    {
        // If the generic argument has been successfully resolved, use it;
        // otherwise, leave the open-constructe argument in place.

        if (resolvedGenericParameters.ContainsKey(openConstructedGenericArgument))
        {
            return resolvedGenericParameters[openConstructedGenericArgument];
        }
        else
        {
            return openConstructedGenericArgument;
        }
    }).ToArray();

    return openConstructedMethod.MakeGenericMethod(closedConstructedGenericArguments);
}
Lazlo
  • 8,518
  • 14
  • 77
  • 116
2

Looking a little bit online for something like this, I found this article by Scott Hanselman. After reading it (it's short), and already thinking along the lines of the extension method from @Jon Skeet's answer, I threw this little tidbit together and gave it a quick run:

public static class Extensions
{
    public static bool IsImplementationOf(this System.Type objectType, System.Type interfaceType)
    {
        return (objectType.GetInterface(interfaceType.FullName) != null);
    }
}

It actually worked for the few tests that I put it to. It returned true when I used it on a type that DID implement an interface I passed it, and it failed when I passed it a type that didn't implement the interface. I even removed the interface declaration from the successful type and tried it again and it failed. I used it like this:

if (myType.IsImplementationOf(typeof(IFormWithWorker)))
{
    //Do Something
    MessageBox.Show(myType.GetInterface(typeof(DocumentDistributor.Library.IFormWithWorker).FullName).FullName);
}
else
{
    MessageBox.Show("It IS null");
}

I'll probably play around with it but I may end up posting it to: What are your favorite extension methods for C#? (codeplex.com/extensionoverflow)

Wai Ha Lee
  • 8,598
  • 83
  • 57
  • 92
Joel Etherton
  • 37,325
  • 10
  • 89
  • 104