Albireo
Albireo

Reputation: 11095

PostSharp's OnExceptionAspect is corrupting the stack traces line numbers

We have a PostSharp's OnExceptionAspect applied to every method of our project which is corrupting the line numbers reported in the stack traces: the inner stack frame line number is no longer pointing to the line where the exception happened but to the closing brace of the method where the exception happened.

This seems to be a known limitation of Windows which, when rethrowing an exception, resets the stack trace origin (see Incorrect stacktrace by rethrow).

You can reproduce this issue with this code (you need PostSharp installed):

namespace ConsoleApplication1
{
    using System;

    using PostSharp.Aspects;

    public static class Program
    {
        public static void Main()
        {
            try
            {
                Foo(2);
            }
            catch (Exception exception)
            {
                var type = exception.GetType();

                Console.Write(type.FullName);
                Console.Write(" - ");
                Console.WriteLine(exception.Message);
                Console.WriteLine(exception.StackTrace);
            }
        }

        private static void Foo(int value)
        {
            if (value % 2 == 0)
            {
                throw new Exception("Invalid value.");
            }

            Console.WriteLine("Hello, world.");
        }
    }

    [Serializable]
    public class LogExceptionAspect : OnExceptionAspect
    {
        public override void OnException(MethodExecutionArgs methodExecutionArgs)
        {
        }
    }
}

Executing this code gives the following stack trace:

System.Exception - Invalid value.
   at ConsoleApplication1.Program.Foo(Int32 value) in …\Program.cs:line 36
   at ConsoleApplication1.Program.Main() in …\Program.cs:line 15

Line 36 is not throw new Exception("Invalid value."); but the closing brace of private static void Foo(int value).

A solution is to wrap the exception in a new one and rethrow it inside the OnException method of the OnExceptionAspect:

[assembly: ConsoleApplication1.LogExceptionAspect]

namespace ConsoleApplication1
{
    using System;

    using PostSharp.Aspects;

    public static class Program
    {
        public static void Main()
        {
            try
            {
                Foo(2);
            }
            catch (Exception exception)
            {
                while (exception != null)
                {
                    var type = exception.GetType();

                    Console.Write(type.FullName);
                    Console.Write(" - ");
                    Console.WriteLine(exception.Message);
                    Console.WriteLine(exception.StackTrace);

                    exception = exception.InnerException;
                }
            }
        }

        private static void Foo(int value)
        {
            if (value % 2 == 0)
            {
                throw new Exception("Invalid value.");
            }

            Console.WriteLine("Hello, world.");
        }
    }

    [Serializable]
    public class LogExceptionAspect : OnExceptionAspect
    {
        public override void OnException(MethodExecutionArgs methodExecutionArgs)
        {
            throw new Exception("Foo", methodExecutionArgs.Exception);
        }
    }
}

This gives the correct line numbers (throw new Exception("Invalid value."); is on line 37 now):

System.Exception - Foo
   at ConsoleApplication1.LogExceptionAspect.OnException(MethodExecutionArgs methodExecutionArgs) in …\Program.cs:line 49
   at ConsoleApplication1.Program.Foo(Int32 value) in …\Program.cs:line 41
   at ConsoleApplication1.Program.Main() in …\Program.cs:line 15
System.Exception - Invalid value.
   at ConsoleApplication1.Program.Foo(Int32 value) in …\Program.cs:line 37

However this solution is adding garbage to the stack traces (the System.Exception - Foo entry should not really exist) and for us is making them nearly useless (remember that the aspect is applied to every method in our project: so if an exception bubbles up twenty methods we have twenty new nested exceptions added to the stack trace).

Given that we can't — cough PHB cough — get rid of the aspect, which alternatives do we have to have correct line numbers and readable stack traces?

Upvotes: 0

Views: 552

Answers (2)

Albireo
Albireo

Reputation: 11095

As stated by Daniel Balas and Incorrect stacktrace by rethrow, due to a "feature" of the CLR there is no way to preserve the original stack trace when rethrowing an exception raised in the same method.

The following examples show how we implemented a work around by wrapping the original exception in the first stack frame (as mentioned in my comment).

The instruction causing the exception — throw new Exception("Invalid value."); — is on line 41 in both examples.

Before:

[assembly: Sandbox.TrapExceptionAspect]

namespace Sandbox
{
    using System;
    using System.Runtime.Serialization;

    using PostSharp.Aspects;

    public static class Program
    {
        public static void Main()
        {
            try
            {
                Foo(2);
            }
            catch (Exception exception)
            {
                while (exception != null)
                {
                    Console.WriteLine(exception.Message);
                    Console.WriteLine(exception.StackTrace);

                    exception = exception.InnerException;
                }
            }

            Console.ReadKey(true);
        }

        private static void Foo(int value)
        {
            Bar(value);
        }

        private static void Bar(int value)
        {
            if (value % 2 == 0)
            {
                throw new Exception("Invalid value.");
            }

            Console.WriteLine("Hello, world.");
        }
    }

    [Serializable]
    public class TrapExceptionAspect : OnExceptionAspect
    {
        public override void OnException(MethodExecutionArgs args)
        {
        }
    }
}

Stack trace:

Invalid value.
   at Sandbox.Program.Bar(Int32 value) in …\Sandbox\Sandbox\Program.cs:line 45
   at Sandbox.Program.Foo(Int32 value) in …\Sandbox\Sandbox\Program.cs:line 35
   at Sandbox.Program.Main() in …\Sandbox\Sandbox\Program.cs:line 16

After:

[assembly: Sandbox.TrapExceptionAspect]

namespace Sandbox
{
    using System;
    using System.Runtime.Serialization;

    using PostSharp.Aspects;

    public static class Program
    {
        public static void Main()
        {
            try
            {
                Foo(2);
            }
            catch (Exception exception)
            {
                while (exception != null)
                {
                    Console.WriteLine(exception.Message);
                    Console.WriteLine(exception.StackTrace);

                    exception = exception.InnerException;
                }
            }

            Console.ReadKey(true);
        }

        private static void Foo(int value)
        {
            Bar(value);
        }

        private static void Bar(int value)
        {
            if (value % 2 == 0)
            {
                throw new Exception("Invalid value.");
            }

            Console.WriteLine("Hello, world.");
        }
    }

    [Serializable]
    public class TrapExceptionAspect : OnExceptionAspect
    {
        public override void OnException(MethodExecutionArgs args)
        {
            if (args.Exception is CustomException)
            {
                return;
            }

            args.FlowBehavior = FlowBehavior.ThrowException;

            args.Exception = new CustomException("Unhandled exception.", args.Exception);
        }
    }

    [Serializable]
    public class CustomException : Exception
    {
        public CustomException() : base()
        {
        }

        public CustomException(string message) : base(message)
        {
        }

        public CustomException(string message, Exception innerException) : base(message, innerException)
        {
        }

        public CustomException(SerializationInfo info, StreamingContext context) : base(info, context)
        {
        }
    }
}

Stack trace:

Unhandled exception.
   at Sandbox.Program.Bar(Int32 value) in …\Sandbox\Sandbox\Program.cs:line 45
   at Sandbox.Program.Foo(Int32 value) in …\Sandbox\Sandbox\Program.cs:line 35
   at Sandbox.Program.Main() in …\Sandbox\Sandbox\Program.cs:line 16
Invalid value.
   at Sandbox.Program.Bar(Int32 value) in …\Sandbox\Sandbox\Program.cs:line 41

Upvotes: 2

Daniel Balas
Daniel Balas

Reputation: 1850

I'm one of PostSharp's developers. This is a known issue (or rather a feature) of CLR's rethrow instruction. For brevity, it changes the stack trace based on the sequence point of that instruction. The same happens if you use throw; in a catch statement, but that's more apparent as you see the statement that causes it.

We are working on a major change that should provide a fix of this behavior and hopefully would be released in a future version (I cannot tell which one at this point). I'm afraid that until it is released the workaround that you are using (or a similar one) is the only possible solution.

Upvotes: 3

Related Questions