Можно ли получить хорошую трассировку стека с помощью методов .NET async?

У меня есть следующая настройка кода кода в приложении WebApi:

[HttpGet]
public double GetValueAction()
{
    return this.GetValue().Result;
}

public async Task<double> GetValue()
{
    return await this.GetValue2().ConfigureAwait(false);
}

public async Task<double> GetValue2()
{
    throw new InvalidOperationException("Couldn't get value!");
}

К сожалению, когда GetValueAction попадает, возвращается трассировка стека:

    " at MyProject.Controllers.ValuesController.<GetValue2>d__3.MoveNext() in c:\dev\MyProject\MyProject\Controllers\ValuesController.cs:line 61 --- End of stack trace from previous location where exception was thrown --- 
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) 
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) 
at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
at MyProject.Controllers.ValuesController.<GetValue>d__0.MoveNext() in c:\dev\MyProject\MyProject\Controllers\ValuesController.cs:line 56"

Таким образом, я получаю (искаженные) GetValue2 и GetValue в трассировке, но не упоминает GetValueAction. Я делаю что-то неправильно? Есть ли еще один шаблон, который даст мне более полные следы стека?

EDIT: моя цель - не писать код, основанный на трассировке стека, а вместо этого облегчать отладку отказов в асинхронных методах.

Ответ 1

Во-первых, следы стека не делают то, что думают большинство людей. Они могут быть полезны во время отладки, но не предназначены для использования во время выполнения, особенно на ASP.NET.

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

Итак, фрейм стека не является правильным ответом для ваших нужд. Эрик Липперт объясняет это в своем ответе здесь.

статья MSDN, связанная с @ColeCampbell, описывает один из способов отслеживания "цепочек несчастных случаев" (откуда пришел код) с помощью async код. К сожалению, этот подход ограничен (например, он не обрабатывает сценарии fork/join); однако это единственный подход, который я знаю о том, что он работает в приложениях Windows Store.

Поскольку вы работаете на ASP.NET с полной версией .NET 4.5, у вас есть доступ к более мощному решению для отслеживания цепочек случайности: контекст логического вызова. Тем не менее, ваши методы async должны "выбирать", поэтому вы не получите их бесплатно, как в случае трассировки стека. Я просто написал это в блоге, который еще не опубликован, поэтому вы получаете предварительный просмотр.:)

Вы можете построить "стек" вызовов самостоятельно вокруг контекста логического вызова как такового:

public static class MyStack
{
  // (Part A) Provide strongly-typed access to the current stack
  private static readonly string slotName = Guid.NewGuid().ToString("N");
  private static ImmutableStack<string> CurrentStack
  {
    get
    {
      var ret = CallContext.LogicalGetData(name) as ImmutableStack<string>;
      return ret ?? ImmutableStack.Create<string>();
    }
    set { CallContext.LogicalSetData(name, value); }
  }

  // (Part B) Provide an API appropriate for pushing and popping the stack
  public static IDisposable Push([CallerMemberName] string context = "")
  {
    CurrentStack = CurrentStack.Push(context);
    return new PopWhenDisposed();
  }
  private static void Pop() { CurrentContext = CurrentContext.Pop(); }
  private sealed class PopWhenDisposed : IDisposable
  {
    private bool disposed;
    public void Dispose()
    {
      if (disposed) return;
      Pop();
      disposed = true;
    }
  }

  // (Part C) Provide an API to read the current stack.
  public static string CurrentStackString
  {
    get { return string.Join(" ", CurrentStack.Reverse()); }
  }
}

(ImmutableStackдоступен здесь). Затем вы можете использовать его следующим образом:

static async Task SomeWork()
{
  using (MyStack.Push())
  {
    ...
    Console.WriteLine(MyStack.CurrentStackAsString + ": Hi!");
  }
}

Самое приятное в этом подходе заключается в том, что он работает со всеми async: fork/join, custom awaitables, ConfigureAwait(false) и т.д. Недостатком является то, что вы добавляете некоторые накладные расходы. Кроме того, этот подход работает только на .NET 4.5; контекст логического вызова на .NET 4.0 не является async -aware и будет работать не.

Обновление: Я выпустил пакет NuGet (описанный в моем блоге), который использует PostSharp для ввода нажатий и всплывает автоматически. Поэтому получить хороший след теперь будет намного проще.

Ответ 2

Для этого может быть добавлено расширение nuget с помощью цапфы async/wait.

https://www.nuget.org/packages/AsyncStackTraceEx/

вам нужно изменить свой ожидающий вызов из

Await DownloadAsync(url)

к

Await DownloadAsync(url).Log()

Наконец, в блоке catch просто вызовите

ex.StackTraceEx()

Одно важное замечание: этот метод можно вызывать только один раз, а ex.StackTrace не следует оценивать раньше. Кажется, что стек можно прочитать только один раз.