好吧,我 将命名此和上下文问题,但显然标题中不允许使用 question 这个词。
无论如何,问题在于:我在我的WCF服务中使用IErrorHandler
以便提供日志记录而不会混淆我的所有服务代码。到目前为止,这一点非常有效。但是,现在我正在尝试转向完全异步服务,我遇到了call stack
being a return stack instead of a causality chain的问题。
现在,我尝试使用Stephen Cleary's logical call context MyStack,并结合Ninject
的{{1}}扩展名。
Ninject:
Intercept
SimpleContextGenerator:
Bind<IThing>().To<Thing>()
.Intercept()
.With<SimpleContextGenerator>();
然而,问题有两个:1)public class SimpleContextGenerator : IInterceptor
{
public void Intercept(IInvocation invocation)
{
using (MyStack.Push(
string.Join(".",
invocation.Request.Method.DeclaringType.FullName,
invocation.Request.Method.Name)))
{
invocation.Proceed();
}
}
}
在错误实际抛出之前完成,2)1甚至不重要因为整个上下文在我到达{{{{ 1}}。我可以在using
中注明IErrorHandler
中的代码,Pop
中的MyStack
CurrentContext.IsEmpty
时注释true
。
所以,我的问题也是两部分:
1)有没有办法将上下文保持到ProvideFault
来电?
2)如果没有,是否有其他方法可以在全球范围内记录 有权访问上下文的错误?
我使用的是.NET 4.5,Ninject 3.2和DynamicProxy 3.2。
说实话,我很高兴只知道抛出异常的地方 - 目前的课程和方法足以达到我的目的;不需要完整堆栈。
编辑:如果我使用IErrorHandler
将其放入IErrorHandler
,我可以保留它,直到我到达OperationContext
。但是,我仍然不知道方法何时结束,所以我无法确定异常发生的位置。
答案 0 :(得分:1)
为了以IErrorHandler
中提供的方式跟踪堆栈,请使用IExtension<>
:
public class ContextStack : IExtension<OperationContext>
{
// http://stackoverflow.com/a/1895958/128217
private readonly LinkedList<Frame> _stack;
private ContextStack()
{
_stack = new LinkedList<Frame>();
}
public LinkedList<Frame> Stack
{
get { return _stack; }
}
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private static readonly object _locker = new object();
public static ContextStack Current
{
get
{
ContextStack context = OperationContext.Current.Extensions.Find<ContextStack>();
if (context == null)
{
lock (_locker)
{
context = OperationContext.Current.Extensions.Find<ContextStack>();
if (context == null)
{
context = new ContextStack();
OperationContext.Current.Extensions.Add(context);
}
}
}
return context;
}
}
public IDisposable Push(Frame frame)
{
Stack.AddFirst(frame);
return new PopWhenDisposed(frame, Stack);
}
public void Attach(OperationContext owner) { }
public void Detach(OperationContext owner) { }
private sealed class PopWhenDisposed : IDisposable
{
private bool _disposed;
private readonly Frame _frame;
private readonly LinkedList<Frame> _stack;
public PopWhenDisposed(Frame frame, LinkedList<Frame> stack)
{
_frame = frame;
_stack = stack;
}
public void Dispose()
{
if (_disposed)
{
return;
}
_stack.Remove(_frame);
_disposed = true;
}
}
}
这里是正在跟踪的Frame
:
public class Frame
{
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly string _type;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly string _method;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly Parameter[] _parameters;
public string Type { get { return _type; } }
public string Method { get { return _method; } }
public Parameter[] Parameters { get { return _parameters; } }
public Task Task { get; private set; }
public Exception Exception { get; private set; }
public Frame(Type type, string method, params Parameter[] parameters)
{
_type = type.FullName;
_method = method;
_parameters = parameters;
}
public void SetTask(Task task)
{
if (Task != null)
{
throw new InvalidOperationException("Task is already set.");
}
Task = task;
}
public void SetException(Exception exception)
{
if (Exception != null)
{
throw new InvalidOperationException("Exception is already set.");
}
// Unwrap AggregateExceptions with a single inner exception.
if (exception is AggregateException && ((AggregateException)exception).InnerExceptions.Count == 1)
{
Exception = exception.InnerException;
}
else
{
Exception = exception;
}
}
public override string ToString()
{
StringBuilder sb = new StringBuilder(Type);
sb.Append(".");
sb.Append(Method);
sb.Append("(");
sb.Append(string.Join(", ", (object[])Parameters)); // Needed to pick an overload.
sb.Append(")");
return sb.ToString();
}
}
Parameter
:
public class Parameter
{
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly string _name;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly string _type;
public string Name { get { return _name; } }
public string Type { get { return _type; } }
public Parameter(string name, Type type)
{
_name = name;
_type = type.Name;
}
public override string ToString()
{
return string.Format("{0} {1}", Type, Name);
}
}
现在,您想使用此SimpleContextGenerator
:
public class SimpleContextGenerator : IInterceptor
{
public void Intercept(IInvocation invocation)
{
OperationContextSynchronizationContext synchronizationContext = null;
try
{
// Build the logical call stack by storing the current method being called
// in our custom context stack. Note that only calls made through tracked
// interfaces end up on the stack, so we may miss some details (such as calls
// within the implementing class).
var stack = ContextStack.Current;
Frame frame = new Frame(
invocation.Request.Target.GetType(),
invocation.Request.Method.Name,
invocation.Request.Method.GetParameters().Select(param => new Parameter(param.Name, param.ParameterType)).ToArray());
var dispose = stack.Push(frame);
// Make sure that the OperationContext flows across to deeper calls,
// since we need it for ContextStack. (And also it's cool to have it.)
synchronizationContext = new OperationContextSynchronizationContext(frame);
// Process the method being called.
try
{
invocation.Proceed();
}
catch (Exception ex)
{
frame.SetException(ex);
throw;
}
var returnType = invocation.Request.Method.ReturnType;
if (returnType == typeof(Task)
|| (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>)))
{
Task task = invocation.ReturnValue as Task; // Could be a Task or a Task<>, and we honestly don't really care which.
frame.SetTask(task);
task.ContinueWith(t =>
{
// If we've succeeded, then remove.
if (!t.IsFaulted)
{
dispose.Dispose();
}
else
{
frame.SetException(t.Exception);
}
});
}
else
{
// If we're not returning a Task, that means that we've fully processed the method.
// This will be hit for async void methods as well (which are, as far as we're
// concerned, fully processed).
dispose.Dispose();
}
}
finally
{
//SynchronizationContext.SetSynchronizationContext(original);
if (synchronizationContext != null)
{
synchronizationContext.Dispose();
}
}
}
}
IInterceptor
这里是Ninject.Extensions.Interception.IInterceptor
。
为了保持每次通话的OperationContext
可用,您需要使用此OperationContextSynchronizationContext
:
public class OperationContextSynchronizationContext : SynchronizationContext, IDisposable
{
// Track the operation context to make sure that it flows through to the next call context.
private readonly Frame _currentFrame;
private readonly OperationContext _context;
private readonly SynchronizationContext _previous;
public OperationContextSynchronizationContext(Frame currentFrame)
{
_currentFrame = currentFrame;
_context = OperationContext.Current;
_previous = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(this);
}
public override void Post(SendOrPostCallback d, object state)
{
var context = _previous ?? new SynchronizationContext();
context.Post(
s =>
{
OperationContext.Current = _context;
try
{
d(s);
}
catch (Exception ex)
{
// If we didn't have this, async void would be bad news bears.
// Since async void is "fire and forget," they happen separate
// from the main call stack. We're logging this separately so
// that they don't affect the main call (and it just makes sense).
// implement your logging here
}
},
state
);
}
private bool _disposed = false;
public void Dispose()
{
if (!_disposed)
{
// Return to the previous context.
SynchronizationContext.SetSynchronizationContext(_previous);
_disposed = true;
}
}
}
然后你只需要在Ninject
绑定中将它全部挂钩:
Bind<IBusinessLayer>().To<BusinessLayer>()
.Intercept().With<SimpleContextGenerator>(); // Track all logical calls.
请注意,这可以仅应用于接口到具体类绑定,这就是我们无法以这种方式将服务本身放入堆栈的原因。我们可以包装每个服务方法(比包装每个调用更好),但我认为我们甚至不能用模块来做,因为服务框架不会有堆栈遍历的异常(下面) )。
最后,在IErrorHandler
:
var context = ContextStack.Current.Stack;
if (context.Any())
{
// Get all tasks that haven't yet completed and run them. This will clear out any stack entries
// that don't error. This will run at most once; there should not be any situation where it
// would run more than once. As such, not doing a loop, though, if we find a situation where it
// SHOULD run more than once, we should put the loop back in (but with a check for max loops).
var frames = context.Where(frame => frame.Task != null && !frame.Task.IsCompleted);
//while (tasks.Any())
//{
foreach (var frame in frames.ToList()) // Evaluate to prevent the collection from being modified while we're running the foreach.
{
// Make sure that each task has completed. This may not be super efficient, but it
// does allow each method to complete before we log, meaning that we'll have a good
// indication of where all the errors are, and that seems worth it to me.
// However, from what I've seen of the state of items that get here, it doesn't look
// like anything here should error.
try
{
frame.Task.Wait();
}
catch (Exception taskEx)
{
frame.SetException(taskEx);
}
}
//}
}
// Prepare error information for one or more errors.
// Always use the frames instead of the one that got us here,
// since we have better information in the frames.
var errorFrames = context.Where(frame => frame.Exception != null);
if (errorFrames.Any())
{
// Unpack all exceptions so we have access to every actual exception in each frame.
var unpackedErrorFrames = errorFrames.GroupBy(frame => frame.Exception.Unpack())
.Select(group => new { Frame = group.First(), Exceptions = group.Key });
// Expand out the exceptions.
var expandedFrames = (from frame in unpackedErrorFrames
from exception in frame.Exceptions
select new { Frame = frame.Frame, Exception = exception });
// Walk the stack.
// The stack does not currently have the service itself in it, because I don't have an easy way to
// wrap the service call to track the service frame and exception..
var errorStacks = expandedFrames.GroupBy(frame => frame.Exception)
.Select(group => new { Exception = group.Key, Stack = group.ToList() });
// Log all exceptions.
foreach (var stack in errorStacks)
{
var exception = stack.Exception;
var @class = stack.Stack.First().Type;
var method = stack.Stack.First().Method;
var exceptionStack = stack.Stack.SelecT(s => s.Frame);
// log exception here.
}
}
else
{
// Well, if we don't have any error frames, but we still got here with an exception,
// at least log that exception so that we know.
// Since the service itself isn't in the stack, we'll get here if there are any
// exceptions before we call the business layer.
// log error here
}
这是Unpack
扩展方法:
public static IEnumerable<Exception> Unpack(this Exception exception)
{
List<Exception> exceptions = new List<Exception>();
var agg = exception as AggregateException;
if (agg != null)
{
// Never add an AggregateException.
foreach (var ex in agg.InnerExceptions)
{
exceptions.AddRange(ex.Unpack());
}
}
else
{
exceptions.Add(exception);
}
return exceptions;
}