Как получить правильную трассировку стека для исключений в асинхронных действиях Web API 2?

У меня есть простой метод API-контроллера

public async Task<Models.Timesheet> GetByDate(DateTime date, string user = null)
{
    throw new InvalidOperationException();
}

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

System.InvalidOperationException: Operation is not valid due to the current state of the object.
   at System.Web.Http.Filters.ActionFilterAttribute.<CallOnActionExecutedAsync>d__1.MoveNext()
--- 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.TaskAwaiter`1.GetResult()
   at System.Web.Http.Controllers.ActionFilterResult.<ExecuteAsync>d__2.MoveNext()
--- 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.Web.Http.ApiController.<InvokeActionWithExceptionFilters>d__1.MoveNext()

Раньше он был намного лучше с Web API v1. После обновления до v2 трассировки стека в значительной степени непригодны - ожидается, что при асинхронном/ожидании трассировки стека не будут такими, какими они когда-то были, но в этом случае вся трассировка стека не содержит намека на даже класс, который не смогли. Есть ли что-то, что можно настроить в веб-API для устранения проблемы?

Ответ 2

Итак, после того, как я задал вопрос, я получил правильную мотивацию для правильного копания кода веб-API, чтобы найти проблему. Похоже, что виновником является ActionFilterAttribute.CallOnActionExecutedAsync метод, который делает что-то вроде этого (перефразируемый, а не фактический код):

try
{
    await previous();
}
catch(Exception ex)
{
    exception = ex;
}

var context = new HttpActionExecutedContext(actionContext, exception);
this.OnActionExecuted(context);
if (context.Response == null && context.Exception != null)
    throw context.Exception;

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

Это заставило меня понять, что вместо получения ExceptionFilterAttribute мне нужно получить из ActionFilterAttribute, так как первые всегда называются последними. К сожалению, мне также необходимо убедиться, что мой фильтр исключений работает первым, но это не встроенный веб-API (см. Этот вопрос для правильного решения для этого).

Мое быстрое решение состояло в том, чтобы убедиться, что все фильтры, которые у меня есть в приложении (у меня есть только несколько), получены из моего настраиваемого фильтра исключений (и вызывают base.OnActionExecuted()), поэтому каждый отдельный фильтр будет проверять исключение и выполнять что-то например:

public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
    if (actionExecutedContext.Exception == null)
        return;

    WriteLog(actionExecutedContext.Exception);

    actionExecutedContext.Response = actionExecutedContext.Request.CreateErrorResponse(
        System.Net.HttpStatusCode.InternalServerError,
        actionExecutedContext.Exception.Message,
        actionExecutedContext.Exception);

    actionExecutedContext.Exception = null;
}

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

Ответ 3

Я считаю, что трассировки стека улучшены на .NET 4.5.1 при работе в Windows 8.1/Server 2012 R2.

В качестве альтернативы у меня есть "Async Diagnostics" пакет NuGet, который вы можете установить в свой проект. Затем добавьте эту строку:

[assembly: AsyncDiagnosticAspect]

И вы можете использовать метод расширения ToAsyncDiagnosticString для типа Exception. ToAsyncDiagnosticString включает всю информацию из ToString, а затем добавляет "логический стек". Дополнительная документация (и источник) на GitHub.

Обратите внимание, что поддержка частичного доверия отсутствует, и это лучше всего работает в сборках Debug.