Search code examples
c#stack-tracestack-framemethodinfo

Get source file / line from MethodInfo


Say I have a method PrintSourceCode(Action action) that accepts an Action delegate and is executed this way:

PrintSourceCode(() => 
{ // need this line number
    DoSomething();
})

Within PrintSourceCode I would like to print the source code of the action passed.

I was able to get the StackFrame, then read the source file and output it. However it produces the output of the entire PrintSourceCode() call rather that just the action.

        static void PrintSourceCode(Action action)
        {
            action();
            
            var mi = action.GetMethodInfo(); // not currently used

            var frame = new StackFrame(1, true);
            var lineNumber = frame.GetFileLineNumber();
            var fileName = frame.GetFileName();
            var lines = File.ReadAllLines(fileName);
            var result = new StringBuilder();

            for (var currLineIndex = lineNumber - 1; currLineIndex < lines.Length; currLineIndex++)
            {
                var currentLine = lines[currLineIndex];
                result.AppendLine(currentLine);
                if (Regex.IsMatch(currentLine, @";\s*$"))
                {
                    break;
                }
            }

            Console.WriteLine(result.ToString());
        }

Current output

PrintSourceCode(() =>
{
     DoSomething();

Desired output

{
     DoSomething();
}

I can call action.GetMethodInfo() however that doesn't seem to have the file/line information. And there is no way to get/construct the StackFrame because the action is not running.

Is there a way to get the file/line of an action from the outside of the action itself?


Solution

  • I think this is what you're after. Hopefully helps you out.

    Result: Console Output

    Full Code:

    public class TestPrintAction
    {
        static readonly Regex regexActionBody = new Regex(@"(\{(\n.*)*\})\);");
    
        public static void Main()
        {
            PrintSourceCode(() => {
                Console.WriteLine("Hello World");
    
            });
        }
    
    
        static void PrintSourceCode(
            Action action,
            [System.Runtime.CompilerServices.CallerMemberName] string membername = "",
            [System.Runtime.CompilerServices.CallerFilePath] string filepath = "",
            [System.Runtime.CompilerServices.CallerLineNumber] int linenumber = 0)
        {
            Match _match = Match.Empty;
            string[] _fileLines;
            string[] _fileSubLines;
            string _fileSubText;
            string _out;
    
            //Uncomment To Run The Action
            //action();
    
            _fileLines = File.ReadLines(filepath).ToArray();
    
            _fileSubLines =
                _fileLines.Skip(linenumber-1)
                .ToArray();
    
            _fileSubText = string.Join(
                separator: "\n",
                value: _fileSubLines);
    
            try
            {
                _match =
                    regexActionBody.Match(_fileSubText);
    
                _out =
                    (_match.Success)
                    ? _match.Groups[1].Value.ToString()
                    : string.Empty;
    
            }
            catch (Exception ex)
            {
                Debugger.Break();
                _out = string.Empty;
    
            }
    
            Console.WriteLine(""
                + $"Caller Member Name: {membername}\n"
                + $"Caller File Path: {filepath}\n"
                + $"Caller Line Number: {linenumber}\n"
                + $"Action Body: {_out}"
                );
        }
    }
    

    Notes:

    Utilize attributes from System.Runtime.CompilerServices in the PrintSourceCode method arguments to get file path and line number.

    You can comment back in the Action if you need to run it in the PrintSourceCode method.

    I added this to clear out the code before the call to PrintSourceCode. Allowed me to make the regex a little simpler. Suppose you could have a reader function to parse the Action Body, would not be as expensive as the Regex, whatever works best for your end result :-)

            _fileSubLines =
                _fileLines.Skip(linenumber-1)
                .ToArray();
    

    Yes you can condense a lot of this. I just broke it out to show a little more depth to what is going on.