Jason W
Jason W

Reputation: 13209

Extension methods with base and sub-classes

UPDATE

Requesting to re-open because the other SO answers don't have a solution, but one of the comments to the question has a solution I want to accept as it works for the scenario.

Original Question

I am having trouble writing extension methods with non-abstract base classes and sub-classes that select the appropriate extension method.

Example Code

I have a very simple example below (abstracted from a much bigger project) that uses the extension method "Run". The expected output is listed in the comment next to each class.

public class Parent { }; // Should output "Parent"
public class ChildA : Parent { }; // Should output "Child A"
public class ChildB : Parent { }; // Should output "Parent"

// Expected Output: ChildA, Parent, Parent
public class Program
{
    public static void Main()
    {
        var commands = new List<Parent>() { new ChildA(), new ChildB(), new Parent() };
        Console.WriteLine(string.Join(", ", commands.Select(c => c.Run())));
    }
}

Here are my attempts so far, but there has to be a cleaner way to do this:

  1. No type-checking - Results in the Parent extension method being used exclusively (Parent, Parent, Parent)
  2. Explicit type checking - Right output, but have to explicitly check types for each extension possibility (ChildA, Parent, Parent)
  3. Attempted Convert.ChangeType to dynamic type - Run-time exception since extension won't catch the dynamic type (no output)
  4. Attempted generic cast with reflection - not quite operational yet, but not sure if approach is even valid

List of attempts

public static class Extensions
{
    public static string Run(this ChildA model)
    {
        return "ChildA";
    }
    public static string Run(this Parent model)
    {
        return model.Run1(); // Change to test different approaches
    }
    public static string Run1(this Parent model) // No type-checking
    {
        return "Parent";
    }
    public static string Run2(this Parent model) // Explicitly check sub-types
    {
        if (model is ChildA)
            return ((ChildA)model).Run();
        else
            return "Parent";
    }
    public static string Run3(this Parent model) // Attempted dynamic type conversion
    {
        if (model.GetType().BaseType == typeof(Parent))
        {
            dynamic changedObj = Convert.ChangeType(model, model.GetType());
            return changedObj.Run();
        }
        else
            return "Parent";
    }
    public static string Run4(this Parent model) // Attempted reflected generic type conversion
    {
        if (model.GetType().BaseType == typeof(Parent))
        {
            var method = typeof(Extensions).GetMethod("Cast");
            var generic = method.MakeGenericMethod(new[] { model.GetType() });
            //var generic = generic.Invoke(new object(), null);
            //return generic.Run();
            return "Not working yet";
        }
        else
            return "Parent";
    }
    public static T Cast<T>(this object input)
    {
        return (T) input;   
    }

}

Upvotes: 2

Views: 2718

Answers (2)

ASh
ASh

Reputation: 35730

the best Run method overload is resolved at compile-time and for items of List<Parent> it is Run(this Parent model). Polymorphic behavior can be imitated using reflection in extension method

demonstration

using System;
using System.Reflection;
using System.Linq;
using System.Collections.Generic;

public static class Extensions
{
    private static Dictionary<Type, MethodInfo> _runs;
    private static Type  _parentType;

    static Extensions()
    {
        _parentType = typeof(Parent);        
        _runs = new Dictionary<Type, MethodInfo>();

        // overloads of Run method, which return string for different types derived from Parent
        var methods = typeof(Extensions)
                      .GetMethods(BindingFlags.Static|BindingFlags.Public)
                      .Where(m => m.Name == "Run" && m.ReturnType == typeof(string));

        foreach(var mi in methods)
        {
            var args = mi.GetParameters();
            //  method should have only one parameter
            if (args.Length != 1 || _parentType.IsAssignableFrom(args[0].ParameterType) == false)
                return;         
            _runs.Add(args[0].ParameterType, mi);
        }

    }

// overloads

    public static string Run(this ChildA model)
    {
        return "ChildA";
    }

    public static string Run(this Parent model, object args)
    {
        // this method is not added to _runs (2 parameters)
        return null;
    }

    public static int Run(this ChildC model)
    {
        // this method is not added to _runs (return int)
        return 0;
    }

    public static string Run(this Parent model) // Attempted dynamic type conversion
    {               
        // not really correct
        if (model == null)
            return "Parent";            
        var t = model.GetType();
        if (t == _parentType)       
            return "Parent";
        // invoke overload for type t
        if (_runs.ContainsKey(t))       
            return (string) _runs[t].Invoke(null, new object[] {model});        
        return "Not working yet";
    }       
}

// usage

public class Parent { };          // Should output "Parent"
public class ChildA : Parent { }; // Should output "Child A"
public class ChildB : Parent { }; // Should output "Not working yet"
public class ChildC : Parent { };

public class Program
{
    public static void Main()
    {
        var commands = new List<Parent>() { new ChildA(), new ChildB(), new Parent(),  new ChildC(), (ChildA)null};
        Console.WriteLine(string.Join(", ", commands.Select(c => c.Run())));

        // extension method can be invoked for null
        Console.WriteLine(((ChildA)null).Run());

        //// crashes on (ChildA)null with error: 
        //// The call is ambiguous between the following methods or properties: 'Extensions.Run(ChildA)' and 'Extensions.Run(ChildC)'
        //Console.WriteLine(string.Join(", ", commands.Select(c => Extensions.Run(c as dynamic))));
    }
}

resume

extension method can be invoked as common method (.Run()), not as static Extensions.Run

extension method Run(this Parent model) has problems with null argument (cannot resolve type correct)

trick with dynamic works in most situations, but :

  1. invokes int Run(this ChildC model) method, which returns int not string like others (when (ChildA)null is removed from list)
  2. crashes with The call is ambiguous between the following methods or properties: 'Extensions.Run(ChildA)' and 'Extensions.Run(ChildC)' error (i don't understand that)

Upvotes: 1

Kyle W
Kyle W

Reputation: 3752

Creating the two extension methods for Parent and ChildA, you can move the association to runtime by using dynamic.

Console.WriteLine(string.Join(", ", commands.Select(c => Extensions.Run(c as dynamic)))); 

Upvotes: 2

Related Questions