Подтвердить что ты не робот

Как скрыть текущий метод из трассировки стека исключений в .NET?

Я хотел бы знать, есть ли способ выбросить исключение изнутри метода, но не включать этот метод в трассировку стека исключений. Например.

void ThrowSomeException()
{
    throw new SomeException();
}

И затем, если я вызову этот метод из метода с именем Foo(), я хочу, чтобы трассировка стека исключений начиналась с at Foo(), а не at ThrowSomeException(). Я предполагаю, что если это возможно, это может быть связано с использованием атрибутов метода.

Мне интересен общий ответ, но если это невозможно, то я действительно пытаюсь создать метод расширения AssertEqual() для IEnumerable, который я буду использовать в тестах NUnit. Поэтому, когда я вызываю myEnumerable.AssertEqual(otherEnumerable), и он терпит неудачу, NUnit должен сообщить об ошибке внутри метода тестирования, а не в методе расширения.

Спасибо!

4b9b3361

Ответ 1

Возможно, вы могли бы получить свой собственный тип исключения и переопределить свойство getter StackTrace, чтобы исключить ваш метод:

using System;
using System.Collections.Generic;

class MyException : Exception {

    string _excludeFromStackTrace;

    public MyException(string excludeFromStackTrace) {
        _excludeFromStackTrace = excludeFromStackTrace;
    }

    public override string StackTrace {
        get {
            List<string> stackTrace = new List<string>();
            stackTrace.AddRange(base.StackTrace.Split(new string[] {Environment.NewLine},StringSplitOptions.None));
            stackTrace.RemoveAll(x => x.Contains(_excludeFromStackTrace));
            return string.Join(Environment.NewLine, stackTrace.ToArray());
        }
    }
}

class Program {

    static void TestExc() {
        throw new MyException("Program.TestExc");
    }

    static void foo() {
        TestExc();
    }

    static void Main(params string[] args) {
        try{
            foo();
        } catch (Exception exc){
            Console.WriteLine(exc.StackTrace);
        }
    }

}

Ответ 2

Используя код в конце этого ответа, вы можете написать код, например:

[HideFromStackTrace] // apply this to all methods you want omitted in stack traces
static void ThrowIfNull(object arg, string paramName)
{
    if (arg == null) throw new ArgumentNullException(paramName);
}

static void Foo(object something)
{
    ThrowIfNull(something, nameof(something));
    …
}

static void Main()
{
    try
    {
        Foo(null);
    }
    catch (Exception e)
    {
        Console.WriteLine(e.GetStackTraceWithoutHiddenMethods());
    }                  // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
}                      // gets a stack trace string representation
                       // that excludes all marked methods

Здесь одна возможная реализация:

using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;

[AttributeUsage(AttributeTargets.Method, Inherited=false)]
public class HideFromStackTraceAttribute : Attribute { }

public static class MethodBaseExtensions
{
    public static bool ShouldHideFromStackTrace(this MethodBase method)
    {
        return method.IsDefined(typeof(HideFromStackTraceAttribute), true);
    }
}

public static class ExceptionExtensions
{
    public static string GetStackTraceWithoutHiddenMethods(this Exception e)
    {
        return string.Concat(
            new StackTrace(e, true)
                .GetFrames()
                .Where(frame => !frame.GetMethod().ShouldHideFromStackTrace())
                .Select(frame => new StackTrace(frame).ToString())
                .ToArray());  // ^^^^^^^^^^^^^^^     ^
    }                         // required because you want the usual stack trace
}                             // formatting; StackFrame.ToString() formats differently

Обратите внимание, что это приводит к тому, что отмеченные методы исключаются из одного конкретного представления трассировки стека, а не из самой трассировки стека. Я не знаю, как добиться последнего.

PS: Если вы хотите скрыть метод в окне "Стек вызовов" во время сеанса отладки, просто примените заголовок [DebuggerHidden] атрибут в метод.

Ответ 3

Я предполагаю, что вы хотите сделать это, чтобы объединить код, который используется для создания исключения?
В этом случае вместо написания функции ThrowException() почему бы не написать функцию GetException()? Тогда в Foo просто сделайте throw GetException();

Ответ 4

Ответ метода расширения GetStackTraceWithoutHiddenMethods() прекрасен, за исключением того, что Exception.ToString() не использует свойство StackTrace, вместо этого он вызывает GetStackTrace(), что не является переопределяемым. Поэтому, если вы хотите использовать этот метод расширения со своими собственными типами, основанными на Исключениях, они должны переопределить ToString(), а не переопределять свойство StackTrace.

Ответ 5

Обратите внимание, что это улучшение существующих ответов.


Принятый ответ на этот вопрос действительно неуклюжий, потому что
  1. Он определяет метод, который нам нужно скрыть от трассировки стека по его имени, используя чистую строку.
  2. Разделение трассировки стека основано на методе string.Split.
  3. Он скрывает только один метод от свойства StackTrace, не более.

Но он переопределяет StackTrace свойство StackTrace (которое StackTrace желаемого поведения вопроса)


Наиболее одобренный ответ действительно чище, потому что
  1. он использует атрибут вместо указания имени метода в виде строки.
  2. Его можно использовать, чтобы скрыть более одного метода из StackTrace.

но это действительно сложно и добавить еще два класса только для методов расширения. И самым важным слабым местом в этом не является переопределение StackTrace свойства StackTrace.


Прочитав предыдущие два решения, я думаю, что я нашел самый простой и самый чистый способ (который объединяет лучшее из двух лучших ответов на этот вопрос)

вот инфраструктура, которая нужна.

[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public sealed class StackTraceHiddenAttribute : Attribute
{
}

public class SomeException : Exception
{
    public override string StackTrace
    {
        get
        {
            return string.Concat(
                new StackTrace(this, true)
                    .GetFrames()
                    .Where(frame => !frame.GetMethod().IsDefined(typeof(StackTraceHiddenAttribute), true))
                    .Select(frame => new StackTrace(frame).ToString())
                    .ToArray());
        }
    }
}

и вот пример использования предыдущей инфраструктуры

[StackTraceHidden] // apply this to all methods you want to be omitted in stack traces
static void Throw()
{
    throw new SomeException();
}

static void Foo()
{
    Throw();
}

static void Main()
{
    try
    {
        Foo();
    }
    catch (Exception e)
    {
        Console.WriteLine(e.StackTrace);
    }                  
}      

РЕДАКТИРОВАТЬ В соответствии с комментарием @Stakx на этот ответ, который удаляется сразу после его помещения, он указывает на некоторые важные идеи:
Это решение работает только для пользовательских определенных исключений, а его решение работает со всеми типами исключений, что абсолютно правильно.
В соответствии с этим, здесь есть один метод расширения, не намного более сложный, который может решить проблему и работать со всеми типами исключений.

public static class ExceptionExtensions
{
    public static string GetStackTraceWithoutHiddenMethods(this Exception e)
    {
        return string.Concat(
           new StackTrace(e, true)
               .GetFrames()
               .Where(frame => !frame.GetMethod().IsDefined(typeof(StackTraceHiddenAttribute), true))
               .Select(frame => new StackTrace(frame).ToString())
               .ToArray());
    }                         
}

который почти идентичен его коду, за исключением интеграции метода IsDefined.

Ответ 6

Если вы скажете компилятору агрессивно встроить ваш метод, это должно помешать вашему методу вообще когда-либо попадать в стек вызовов:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
void ThrowSomeException()
{
    throw new SomeException();
}

Этот атрибут доступен с .NET 4.5.

Тем не менее, это только считается сильной подсказкой для компилятора, и в некоторых случаях это все еще не вызывает встраивание. Например, я не думаю, что он может встроить его, если вы вызываете метод из другой сборки или если вы компилируете в режиме отладки.

Одним из обходных путей для этого может быть только создание исключения с вашим помощником и выбросить его из вызывающего кода.

public static InvalidOperationException InvalidOperation(FormattableString message)
{
  return new InvalidOperationException(FormattableString.Invariant(message));
}

// calling code
throw ExceptionHelpers.InvalidOperation($"{0} is not a valid value", value);

Но если ваш вспомогательный метод имеет логику, чтобы определить, следует ли генерировать исключение, это может не сработать для вас:

public static class Require
{
    [ContractAnnotation("condition:false => halt")]
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    [DebuggerHidden]
    public static void True(bool condition)
    {
        if (!condition)
        {
            throw ExceptionHelpers.InvalidOperation($"Expected condition was not met.");
        }
    }
}

В этих случаях вам, вероятно, придется копаться со следами стека исключений, как показывают другие ответы здесь. Например, вы можете игнорировать методы, помеченные DebuggerHiddenAttribute.

Ответ 7

Я создал метод расширения в соответствии с решением StriplingWarrior и работал отлично.

public static class ExceptionExtensions
{
    [DebuggerHidden]
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void Throw(this Exception exception) => throw exception;
}

и тогда мы сможем использовать это...

using static SomeNamespace.ExceptionExtensions;

public class SomeClass
{
    private void SomeMethod(string value)
    {
        var exception = GetArgumentException(nameof(value), value);
        exception?.Throw(); // only throw if any exception was getted

        ... //method implementation
    }

    private Exception GetArgumentException(string paramName, string value)
    {
        if (value == null)
            return new ArgumentNullException(paramName);
        if (string.IsNullOrEmpty(value))
            return new ArgumentException("value is empty.", paramName);

        return null;
    }
}

При этом метод Throw() не будет отображаться в трассировке стека.