Where are the generic parameters saved for Async calls? Where to find its name or other information?
Asked Answered
M

2

6

Here is my test code: the extension method GetInstructions is from here: https://gist.github.com/jbevain/104001

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

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            typeof(TestClass)
             .GetMethods()
             .Where(method => method.Name == "Say" || method.Name == "Hello")
             .ToList()
             .ForEach(method =>
                {
                    var calls = method.GetInstructions()
                    .Select(x => x.Operand as MethodInfo)
                    .Where(x => x != null)
                    .ToList();

                    Console.WriteLine(method);
                    calls.ForEach(call =>
                    {
                        Console.WriteLine($"\t{call}");
                        call.GetGenericArguments().ToList().ForEach(arg => Console.WriteLine($"\t\t{arg.FullName}"));
                    });
                });

            Console.ReadLine();
        }
    }
    class TestClass
    {
        public async Task Say()
        {
            await HelloWorld.Say<IFoo>();
            HelloWorld.Hello<IBar>();
        }

        public void Hello()
        {
            HelloWorld.Say<IFoo>().RunSynchronously();
            HelloWorld.Hello<IBar>();
        }
    }

    class HelloWorld
    {
        public static async Task Say<T>() where T : IBase
        {
            await Task.Run(() => Console.WriteLine($"Hello from {typeof(T)}.")).ConfigureAwait(false);
        }

        public static void Hello<T>() where T : IBase
        {
            Console.WriteLine($"Hello from {typeof(T)}.");
        }
    }
    interface IBase
    {
        Task Hello();
    }

    interface IFoo : IBase
    {

    }

    interface IBar : IBase
    {

    }
}

Here is run result as the screenshot shown:

System.Threading.Tasks.Task Say()
        System.Runtime.CompilerServices.AsyncTaskMethodBuilder Create()
        Void Start[<Say>d__0](<Say>d__0 ByRef)
                ConsoleApp1.TestClass+<Say>d__0
        System.Threading.Tasks.Task get_Task()
Void Hello()
        System.Threading.Tasks.Task Say[IFoo]()
                ConsoleApp1.IFoo
        Void RunSynchronously()
        Void Hello[IBar]()
                ConsoleApp1.IBar

NON-ASYNC calls got correct generic parameters, but ASYNC calls cannot.

enter image description here

My question is: where are the generic parameters stored for ASYNC calls?

Thanks a lot.

Murtha answered 9/9, 2019 at 19:21 Comment(4)
Operand is of type RuntimeMethodInfo, not IFoo. You can find out more about that class here referencesource.microsoft.com/mscorlib/…Footling
@Footling I've read sourcecode before posting this, but it used this a internal function internal string ConstructName from referencesource.microsoft.com/#mscorlib/system/… , which blocked me to find out interface name.Murtha
Check out my answer below, you can call GetGenericArguments() and you can iterate that then find IFoo from that.Footling
@Footling Thanks a lot for your response, I immediately realized I made a wrong example code. my problems are in async code, I cannot get correct parameters from async calls. I've updated my example code. Hopefully you can help again. Thousands of thanks!Murtha
T
8

The async methods are not that easy.

The C# compiler will generate a comprehensive state machine out of an async method. So the body of the TestClass.Say method will be completely overwritten by the compiler. You can read this great blog post if you want to dive deeper into the async state machinery.

Back to your question.

The compiler will replace the method body with something like this:

<Say>d__0 stateMachine = new <Say>d__0();
stateMachine.<>4__this = this;
stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.<>1__state = -1;
AsyncTaskMethodBuilder <>t__builder = stateMachine.<>t__builder;
<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;

<Say>d__0 in this code is a compiler-generated type. It has special characters it its name to prevent you from being able to use this type in your code.

<Say>d__0 is an IAsyncStateMachine implementation. The main logic is contained in its MoveNext method.

It will look similar to this:

TaskAwaiter awaiter;
if (state != 0)
{
    awaiter = HelloWorld.Say<IFoo>().GetAwaiter();
    if (!awaiter.IsCompleted)
    {
        // ...
        builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
        return;
    }
}
else
{
    awaiter = this.awaiter;
    state = -1;
}

awaiter.GetResult();
HelloWorld.Hello<IBar>();

Note that your HelloWorld.Say<IFoo>() call is now here, in this method, not in your original TestClass.Say.

So, to get the generic type information from your method, you will need to inspect the MoveNext state machine method instead of the original TestClass.Say. Search for the call instructions there.

Something like this:

Type asyncStateMachine = 
    typeof(TestClass)
    .GetNestedTypes(BindingFlags.NonPublic)
    .FirstOrDefault(
        t => t.GetCustomAttribute<CompilerGeneratedAttribute>() != null 
        && typeof(IAsyncStateMachine).IsAssignableFrom(t));

MethodInfo method = asyncStateMachine.GetMethod(
    nameof(IAsyncStateMachine.MoveNext),
    BindingFlags.NonPublic | BindingFlags.Instance);

List<MethodInfo> calls = method.GetInstructions()
    .Select(x => x.Operand as MethodInfo)
    .Where(x => x != null)
    .ToList();

// etc

Output:

Void MoveNext()
        System.Threading.Tasks.Task Say[IFoo]()
                ConsoleApp1.IFoo
        System.Runtime.CompilerServices.TaskAwaiter GetAwaiter()
        Boolean get_IsCompleted()
        Void AwaitUnsafeOnCompleted[TaskAwaiter,<Say>d__0](System.Runtime.CompilerServices.TaskAwaiter ByRef, <Say>d__0 ByRef)
                System.Runtime.CompilerServices.TaskAwaiter
                ConsoleApp1.TestClass+<Say>d__0
        Void GetResult()
        Void Hello[IBar]()
                ConsoleApp1.IBar
        Void SetException(System.Exception)
        Void SetResult()

Note that this code depends on current IAsyncStatMachine implementation internals. If the C# compiler changes that internal implementation, this code might break.

Thrice answered 12/9, 2019 at 9:19 Comment(0)
F
2

You can try getting the generic method info and that way you can find the IFoo generic type argument from this (code taken from the msdn):

private static void DisplayGenericMethodInfo(MethodInfo mi)
    {
        Console.WriteLine("\r\n{0}", mi);

        Console.WriteLine("\tIs this a generic method definition? {0}", 
            mi.IsGenericMethodDefinition);

        Console.WriteLine("\tIs it a generic method? {0}", 
            mi.IsGenericMethod);

        Console.WriteLine("\tDoes it have unassigned generic parameters? {0}", 
            mi.ContainsGenericParameters);

        // If this is a generic method, display its type arguments.
        //
        if (mi.IsGenericMethod)
        {
            Type[] typeArguments = mi.GetGenericArguments();

            Console.WriteLine("\tList type arguments ({0}):", 
                typeArguments.Length);

            foreach (Type tParam in typeArguments)
            {
                // IsGenericParameter is true only for generic type
                // parameters.
                //
                if (tParam.IsGenericParameter)
                {
                    Console.WriteLine("\t\t{0}  parameter position {1}" +
                        "\n\t\t   declaring method: {2}",
                        tParam,
                        tParam.GenericParameterPosition,
                        tParam.DeclaringMethod);
                }
                else
                {
                    Console.WriteLine("\t\t{0}", tParam);
                }
            }
        }
    }
Footling answered 9/9, 2019 at 19:54 Comment(6)
Thanks a lot for your response, I immediately realized I made a wrong example code. my problems are in async code, I cannot get correct parameters from async calls. I've updated my example code. Hopefully you can help again. Thousands of thanks!Murtha
Your output looks correct "System.Threading.Tasks.Task Say[IFoo]() ConsoleApp1.IFoo" IFoo is the generic argument you want, no?Footling
No, check out the outputs for first test method TestClass.Say(), which uses async calls, then all parameters go strange.Murtha
I see, let me look into thatFootling
@Murtha sorry man I was unable to find the stack to get IFoo from your code. I ran through several iterations but there is obviously more to AsyncTaskBuilder than I know.Footling
Thanks a lot anyway. async calls are compiled to a different way. it looks the GetInstructions should be improved as well.Murtha

© 2022 - 2024 — McMap. All rights reserved.