lundi 18 janvier 2021

Detect which async overload is called inside a C# method using reflection

Using reflection, I need to determine which overloads a particular async method is calling. In this case, I need to inspect the DoSomething() method to determine which method calls to MyMethod() it's making.

First, consider the following C# console app for the sync version:

using System;
using System.Linq;
using System.Reflection;
using Mono.Cecil;

namespace CecilReflection
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var assemblyDefinition = AssemblyDefinition.ReadAssembly(Assembly.GetExecutingAssembly().Location);

            var toInspect = assemblyDefinition.MainModule
                .GetTypes()
                .SelectMany(type => type.Methods
                    .Where(method => method.HasBody)
                    .Select(method => new
                    {
                        Type = type,
                        Method = method
                    }))
                .Where(x => x.Method.Name == "DoSomething");

            foreach (var entry in toInspect)
            {
                Console.WriteLine($"\tType = {entry.Type.Name}\n\t\tMethod = {entry.Method.Name}");
                foreach (var instruction in entry.Method.Body.Instructions)
                    Console.WriteLine($"{instruction.OpCode} \"{instruction.Operand}\"");
            }

            Console.ReadKey();
        }

        public static void DoSomething()
        {
            var myClass = new MyClass();

            myClass.MyMethod();
            myClass.MyMethod(5);
        }

        public class MyClass
        {
            public void MyMethod()
            {
            }

            public void MyMethod(int arg)
            {
            }
        }
    }
}

Running this produces the following output:

        Type = Program
                Method = DoSomething
nop ""
newobj "System.Void CecilReflection.Program/MyClass::.ctor()"
stloc.0 ""
ldloc.0 ""
callvirt "System.Void CecilReflection.Program/MyClass::MyMethod()"
nop ""
ldloc.0 ""
ldc.i4.5 ""
callvirt "System.Void CecilReflection.Program/MyClass::MyMethod(System.Int32)"
nop ""
ret ""

By looking at the callvirt entries, I'm able to detect that both overloads of MyMethod() are called using the above code.

Here's the async version:

using System;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Mono.Cecil;

namespace CecilReflection
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var assemblyDefinition = AssemblyDefinition.ReadAssembly(Assembly.GetExecutingAssembly().Location);

            var toInspect = assemblyDefinition.MainModule
                .GetTypes()
                .SelectMany(type => type.Methods
                    .Where(method => method.HasBody)
                    .Select(method => new
                    {
                        Type = type,
                        Method = method
                    }))
                .Where(x => x.Method.Name == "DoSomething");

            foreach (var entry in toInspect)
            {
                Console.WriteLine($"\tType = {entry.Type.Name}\n\t\tMethod = {entry.Method.Name}");
                foreach (var instruction in entry.Method.Body.Instructions)
                    Console.WriteLine($"{instruction.OpCode} \"{instruction.Operand}\"");
            }

            Console.ReadKey();
        }

        public static async Task DoSomething()
        {
            var myClass = new MyClass();

            await myClass.MyMethodAsync();
            await myClass.MyMethodAsync(5);
        }

        public class MyClass
        {
            public async Task MyMethodAsync()
            {
            }

            public async Task MyMethodAsync(int arg)
            {
            }
        }
    }
}

This produces the following output:

        Type = Program
                Method = DoSomething
newobj "System.Void CecilReflection.Program/<DoSomething>d__1::.ctor()"
stloc.0 ""
ldloc.0 ""
call "System.Runtime.CompilerServices.AsyncTaskMethodBuilder System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Create()"
stfld "System.Runtime.CompilerServices.AsyncTaskMethodBuilder CecilReflection.Program/<DoSomething>d__1::<>t__builder"
ldloc.0 ""
ldc.i4.m1 ""
stfld "System.Int32 CecilReflection.Program/<DoSomething>d__1::<>1__state"
ldloc.0 ""
ldflda "System.Runtime.CompilerServices.AsyncTaskMethodBuilder CecilReflection.Program/<DoSomething>d__1::<>t__builder"
ldloca.s "V_0"
call "System.Void System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start<CecilReflection.Program/<DoSomething>d__1>(!!0&)"
ldloc.0 ""
ldflda "System.Runtime.CompilerServices.AsyncTaskMethodBuilder CecilReflection.Program/<DoSomething>d__1::<>t__builder"
call "System.Threading.Tasks.Task System.Runtime.CompilerServices.AsyncTaskMethodBuilder::get_Task()"
ret ""

With the async version, I'm not able to determine anything about which overloads to MyMethodAsync() were called, if any.

Is there any way to determine which overloads to MyMethodAsync() were called? It's not a requirement that I use Mono.Cecil, but I've had even less luck using System.Reflection to accomplish this. If a completely different approach would work, that's fine.





Aucun commentaire:

Enregistrer un commentaire