Using reflection to get method name inside an asyn

2020-06-30 03:25发布

问题:

Following is a tiny snippet of code I wrote to demonstrate the basics of this problem.

Code

private async void Form1_Load( object sender, EventArgs e ) {
    var result = await TestAsyncMethodName();
}

private async Task<string> TestAsyncMethodName() {
    string name = "Method: " + System.Reflection.MethodBase.GetCurrentMethod().Name;

    int x = 0;
    foreach ( StackFrame sf in (new StackTrace()).GetFrames()) {
        x++;
        name = name + "\nStack Frame [" + x + "] : " + sf.GetMethod().Name;
    }

    await Task.Run( () => { name = name + "\nAnonymous Method: " + System.Reflection.MethodBase.GetCurrentMethod().Name; } );

    return name;
}

Result

Method: MoveNext
Stack Frame [1] : MoveNext
Stack Frame [2] : Start
Stack Frame [3] : TestAsyncMethodName
Stack Frame [4] : MoveNext
Stack Frame [5] : Start
Stack Frame [6] : Form1_Load
Stack Frame [7] : OnLoad
Stack Frame [8] : OnCreateControl
Stack Frame [9] : CreateControl
Stack Frame [10] : CreateControl
Stack Frame [11] : WmShowWindow
Stack Frame [12] : WndProc
Stack Frame [13] : WndProc
Stack Frame [14] : WmShowWindow
Stack Frame [15] : WndProc
Stack Frame [16] : OnMessage
Stack Frame [17] : WndProc
Stack Frame [18] : DebuggableCallback
Stack Frame [19] : ShowWindow
Stack Frame [20] : SetVisibleCore
Stack Frame [21] : SetVisibleCore
Stack Frame [22] : set_Visible
Stack Frame [23] : RunMessageLoopInner
Stack Frame [24] : RunMessageLoop
Stack Frame [25] : Run
Stack Frame [26] : Main
Stack Frame [27] : _nExecuteAssembly
Stack Frame [28] : ExecuteAssembly
Stack Frame [29] : RunUsersAssembly
Stack Frame [30] : ThreadStart_Context
Stack Frame [31] : RunInternal
Stack Frame [32] : Run
Stack Frame [33] : Run
Stack Frame [34] : ThreadStart
Anonymous Method: <TestAsyncMethodName>b__0

Expected Result

Method: TestAsyncMethodName
Stack Frame [1] : MoveNext
Stack Frame [2] : Start
Stack Frame [3] : TestAsyncMethodName
Stack Frame [4] : MoveNext
Stack Frame [5] : Start
Stack Frame [6] : Form1_Load
Stack Frame [7] : OnLoad
Stack Frame [8] : OnCreateControl
Stack Frame [9] : CreateControl
Stack Frame [10] : CreateControl
Stack Frame [11] : WmShowWindow
Stack Frame [12] : WndProc
Stack Frame [13] : WndProc
Stack Frame [14] : WmShowWindow
Stack Frame [15] : WndProc
Stack Frame [16] : OnMessage
Stack Frame [17] : WndProc
Stack Frame [18] : DebuggableCallback
Stack Frame [19] : ShowWindow
Stack Frame [20] : SetVisibleCore
Stack Frame [21] : SetVisibleCore
Stack Frame [22] : set_Visible
Stack Frame [23] : RunMessageLoopInner
Stack Frame [24] : RunMessageLoop
Stack Frame [25] : Run
Stack Frame [26] : Main
Stack Frame [27] : _nExecuteAssembly
Stack Frame [28] : ExecuteAssembly
Stack Frame [29] : RunUsersAssembly
Stack Frame [30] : ThreadStart_Context
Stack Frame [31] : RunInternal
Stack Frame [32] : Run
Stack Frame [33] : Run
Stack Frame [34] : ThreadStart
Anonymous Method: <TestAsyncMethodName>b__0

References (Research done):

StackOverflow

  • Get Method name that threw exception
  • Can you use reflection to find the name of the currently executing method?
  • How to get the name of the current method from code [duplicate]

Microsoft

  • MethodBase.GetCurrentMethod() Method

回答1:

Change

string name = "Method: " + System.Reflection.MethodBase.GetCurrentMethod().Name;

to

string name = "Method: " + GetActualAsyncMethodName();

Then implement that method as:

static string GetActualAsyncMethodName([CallerMemberName]string name = null) => name;


回答2:

When you use Async your code is converted into a statemachine and so the stack traces and run time information reflects the generated code. If open your assembly where code is in ILSPLY. You method would look like this after generation of statemachine:

    .class nested private auto ansi sealed beforefieldinit '<TestAsyncMethodName>d__2'
    extends [mscorlib]System.Object
    implements [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine
{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    // Fields
    .field public int32 '<>1__state'
    .field public valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string> '<>t__builder'
    .field public class WindowsFormsApp_Recipients.TestForm '<>4__this'
    .field private class WindowsFormsApp_Recipients.TestForm/'<>c__DisplayClass2_0' '<>8__1'
    .field private int32 '<x>5__2'
    .field private class [mscorlib]System.Diagnostics.StackFrame[] '<>s__3'
    .field private int32 '<>s__4'
    .field private class [mscorlib]System.Diagnostics.StackFrame '<sf>5__5'
    .field private valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter '<>u__1'

    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x279e
        // Code size 8 (0x8)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [mscorlib]System.Object::.ctor()
        IL_0006: nop
        IL_0007: ret
    } // end of method '<TestAsyncMethodName>d__2'::.ctor

    .method private final hidebysig newslot virtual 
        instance void MoveNext () cil managed 
    {
        .override method instance void [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext()
        // Method begins at RVA 0x28b0
        // Code size 445 (0x1bd)
        .maxstack 5
        .locals init (
       .................
       .................
       More IL CODE HERE...

As you can see you have a new class type <TestAsyncMethodName>d__2 which wrap the actual logic of your method in generated method MoveNext. So when you ask for System.Reflection.MethodBase.GetCurrentMethod().Name it will give you MoveNext instead of your actual method name.

To get the correct result you can put a hack here:

var regex = new Regex(@"<(\w+)>.*");
string name = "Method: " + regex.Match(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name).Groups[1].Value;

Because the state machine code generation generates a type for the Method name marked as async this Hack will work fine.

The least expensive, and simplest workaround to use at the moment, would be to declare a string variable which acts as a container for the name and then call this as required. And example of this follows :

public async Task<string> Foo() {
     string __FUNCTION__ = "Foo";

     // await / etc code here
}

Another method which is less expensive (memory) than strings, would be to create an enum for your methods and convert that to a string. This would be less expensive in memory when setting the method name, but slightly more expensive when converting back to a string for use. An example of this follows :

public class MyClass {

   public enum __FUNCTIONS {
        Foo,
        Bar
   }

   public async Task<string> Foo() {
        __FUNCTIONS __FUNCTION__ = __FUNCTIONS.Foo;

        // await / etc code here
   }

   public async Task<string> Bar() {
        __FUNCTIONS __FUNCTION__ = __FUNCTIONS.Bar;

        // await / etc code here
   }

}

Unfortunately, there is no real magic constant support in Visual Studio as of this date, and it doesn't appear to be something that will happen in the future as long as the language is a JIT IL, as this technology seems limited to what can be provided through reflection instead of code that appears to be aware of itself (like PHP's __FUNCTION__, etc)