Search code examples
c#genericssystem.reflectionlate-binding

Reflection using generics and late-binding. How to cast at run-time?


I am trying to use Generics with Reflection in c# to build a method that can handle multiple classes. I use a 3rd-party DLL that has a bunch of classes and on those classes, there is a method I call. They all return different return types, but I do the same processing once I get the object back (in my example below, that would be AreaA and AreaB).

Basically, I want to develop a method that takes in the class name and the expected return type as Generic variables and then calls the correct method (methodName) which is supplied as a parameter to this method.

The program below compiles fine and runs without error, but the issue is the expected type of the 'area' variable. In the below statements, the first line is type casted to (TArea), and if I hover over it In Visual Studio, the intellisense shows the property 'name', but typing area.name doesn't give me the value. I have to type ((AreaA)area).name.

Problem is the type 'AreaA' could be another type at run-time. In this example, 'AreaB' so I can hard-code a cast.

How can I accomplish casting the 'area' variable to the appropriate type allowing me to see the public methods/properties of the 3rd-parties class?

NOTE: In my example, eveything is in the same class, but in reality the definitions for ServiceA, ServiceB, AreaA and AreaB will be in a 3rd party DLL.

As always, thanks in advance!

Fig 1 - 'area' variable can only get 'name' property if casted to 'AreaA'

area = (TArea)dfpMethod.Invoke(instance, new object[] { "Area123" });
AreaA areaa = (AreaA)dfpMethod.Invoke(instance, new object[] { "Area123" });

Fig 2. - Complete Program

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;
using System.IO;


namespace ReflectionExample
{
    class Sample
    {
        class ServiceA
        {
            public int size {get; set;}
            public string name {get; set;}

            public ServiceA()
            {
                name = "TestA";
                size = 100;
            }
            public AreaA doWork(string name)
            {
                return new AreaA(name);

            }
        }   

        class AreaA
        {
            public string name { get; set;}
            public AreaA(string name)
            {
                this.name = name;
            }
            public AreaA()
            {

            }

         }

        class ServiceB
        {
            public int size { get; set; }
            public string name { get; set; }

            public ServiceB()
            {
                name = "TestB";
                size = 50;
            }
            public AreaB doWork(string name)
            {
                return new AreaB(name);
            }

        }

        class AreaB
        {
            public string name { get; set; }
            public AreaB(string name)
            {
                this.name = name;
            }
            public AreaB()
            {

            }
        }

        static void Main(string[] args)
        {
            runService<ServiceA, AreaA>("doWork");
        }

        private static void runService<TService, TArea>(string methodName)
            where TService : class, new()
            where TArea : class, new()
        {
            //Compile time processing
            Type areaType = typeof(TArea);  
            Type serviceType = typeof(TService);


            //Print the full assembly name and qualified assembly name
            Console.WriteLine("AreaType--Full assembly name:\t   {0}.", areaType.Assembly.FullName.ToString());            // Print the full assembly name.
            Console.WriteLine("AreaType--Qualified assembly name:\t   {0}.", areaType.AssemblyQualifiedName.ToString());   // Print the qualified assembly name.
            Console.WriteLine("ServiceType--Full assembly name:\t   {0}.", serviceType.Assembly.FullName.ToString());            // Print the full assembly name.
            Console.WriteLine("ServiceType--Qualified assembly name:\t   {0}.", serviceType.AssemblyQualifiedName.ToString());   // Print the qualified assembly name.

            //This is done because in my code, the assembly doesn't reside in the executiy assembly, it is only setup as a reference
            var assembly = Assembly.Load(serviceType.Assembly.FullName);        

            //Initialize the generic area
            TArea area = default(TArea);
            //Get an instance of the service so I can invoke the method later on
            var instance = Activator.CreateInstance(serviceType);

            //Get the methodInfo for the methodName supplied to the runService method
            MethodInfo dfpMethod = serviceType.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance);

            //area is type casted to (TArea), the intellisense shows the property 'name', but typing area.name doesn't give me the value
            //I have to type ((AreaA)area).name.  Problem is the type 'AreaA' could be another type.  In this example, 'AreaB'
            area = (TArea)dfpMethod.Invoke(instance, new object[] { "Area123" });
            AreaA areaa = (AreaA)dfpMethod.Invoke(instance, new object[] { "Area123" });
            Console.WriteLine();

        }

    }
}

Solution

  • The source of your error is that you're casting all of your return values to type TArea with the statement:

    TArea area = (TArea)dfpMethod.Invoke(instance, new object[] { "Area123" });
    

    Per your generic specification, the only thing promised to you by the type TArea is that its a class. Therefore, TArea doesn't give you access to anything but members of the 'object' type.

    Instead, do away with the TArea generic argument in favor of using the 'dynamic' keyword:

    var area = (dynamic)dfpMethod.Invoke(instance, new object[] { "Area123" });
    return area.name; // no error
    

    Note, this is only relevant if the actual types AreaA and AreaB are defined in third party libraries (as you say) and you can't modify them. If you can't modify the classes, you can't introduce an interface (which is what you really need here). If you can't introduce an interface, but all the types expose identical signatures, you can assume the existence of the relevant members using the dynamic type.

    If you need to do a lot of work with AreaA/AreaB and you don't want the performance overhead of all the dynamic operations, define your own generalized class that exposes all the signatures you need:

    public class MyGenericArea 
    {
        public MyGenericArea(string name)
        {
             this.Name = name;
        }
        public string Name {get; set;}
    } 
    

    Then populate the class using the dynamic casting and return that class type instead:

     var area = (dynamic)dfpMethod.Invoke(instance, new object[] { "Area123" });
     return new MyGenericArea(area.name);