检查
Autofac 6.0 引入了诊断支持,形式为 System.Diagnostics.DiagnosticSource 。 这样可以让你拦截 Autofac 的诊断事件。
注意
诊断并非免费的。如果你不将诊断监听器附加到容器,性能会更好。此外,如DefaultDiagnosticTracer
这样的跟踪器在生成操作完整跟踪时会增加内存和资源使用量,因为它们必须在整个解析操作期间保留数据以生成完整的跟踪。建议你在非生产环境中使用诊断;或者使用只处理个别事件而不跟踪完整操作的诊断监听器。
快速入门
要开始使用诊断,最简单的方法是使用 Autofac.Diagnostics.DefaultDiagnosticTracer
类。此跟踪器将生成可用于调试的解析操作的层次结构化跟踪。
// 正常构建你的容器。
var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterType<Component>().As<IService>();
var container = containerBuilder.Build();
// 创建一个追踪器实例,并确定当跟踪准备好查看时要做什么。
var tracer = new DefaultDiagnosticTracer();
tracer.OperationCompleted += (sender, args) =>
{
Trace.WriteLine(args.TraceContent);
};
// 使用你的追踪器订阅诊断。
container.SubscribeToDiagnostics(tracer);
// 正常解析。每当有东西被解析时,追踪器事件就会触发。
using var scope = container.BeginLifetimeScope();
scope.Resolve<IService>();
如果你无法直接访问容器(例如,在 ASP.NET Core 中),可以使用构建回调来注册追踪器。
public void ConfigureContainer(ContainerBuilder builder)
{
// 正常注册 Autofac 的东西。
builder.RegisterModule(new AutofacModule());
// 创建一个追踪器实例,并确定当跟踪准备好查看时要做什么。注意:由于你正在诊断容器,可能不应该同时解析日志记录器,该日志记录器用于记录诊断信息。
var tracer = new DefaultDiagnosticTracer();
tracer.OperationCompleted += (sender, args) =>
{
Console.WriteLine(args.TraceContent);
};
builder.RegisterBuildCallback(c =>
{
var container = c as IContainer;
container.SubscribeToDiagnostics(tracer);
});
}
默认诊断追踪器
上面的快速入门演示了如何使用 Autofac.Diagnostics.DefaultDiagnosticTracer
。
当 OperationCompleted
事件被触发时,你会收到事件参数,这些参数提供了:
Operation
- 完成的实际解析操作,以便在需要时进行检查。OperationSucceeded
- 一个布尔值,指示包含的跟踪是成功还是失败的操作。TraceContent
- 具有完整解析操作跟踪的构建字符串。
假设你有一个简单的 lambda,它注册一个字符串。
var builder = new ContainerBuilder();
builder.Register(ctx => "HelloWorld");
var container = builder.Build();
如果从该容器解析字符串,跟踪看起来像这样:
Resolve Operation Starting
{
Resolve Request Starting
{
Service: System.String
Component: λ:System.String
Pipeline:
-> CircularDependencyDetectorMiddleware
-> ScopeSelectionMiddleware
-> SharingMiddleware
-> RegistrationPipelineInvokeMiddleware
-> ActivatorErrorHandlingMiddleware
-> DisposalTrackingMiddleware
-> λ:System.String
<- λ:System.String
<- DisposalTrackingMiddleware
<- ActivatorErrorHandlingMiddleware
<- RegistrationPipelineInvokeMiddleware
<- SharingMiddleware
<- ScopeSelectionMiddleware
<- CircularDependencyDetectorMiddleware
}
Resolve Request Succeeded; result instance was HelloWorld
}
Operation Succeeded; result instance was HelloWorld
如你所见,跟踪非常详细 - 可以看到解析操作经过的整个 中间件管道 ,可以看到激活器(在这种情况下是一个委托),还可以看到结果实例。
这在尝试解决复杂的解析问题时非常有帮助,尽管跟踪越复杂,信息量越大,可能会让人感到压力。
错误跟踪将包括错误发生的位置并表明失败:
Resolve Operation Starting
{
Resolve Request Starting
{
Service: System.String
Component: λ:System.String
Pipeline:
-> CircularDependencyDetectorMiddleware
-> ScopeSelectionMiddleware
-> SharingMiddleware
-> RegistrationPipelineInvokeMiddleware
-> ActivatorErrorHandlingMiddleware
-> DisposalTrackingMiddleware
-> λ:System.String
X- λ:System.String
X- DisposalTrackingMiddleware
X- ActivatorErrorHandlingMiddleware
X- RegistrationPipelineInvokeMiddleware
X- SharingMiddleware
X- ScopeSelectionMiddleware
X- CircularDependencyDetectorMiddleware
}
Resolve Request FAILED
System.DivideByZeroException: Attempted to divide by zero.
at MyProject.MyNamespace.MyMethod.<>c.<GenerateSimpleTrace>b__6_0(IComponentContext x) in /path/to/MyCode.cs:line 39
at Autofac.RegistrationExtensions.<>c__DisplayClass39_0`1.<Register>b__0(IComponentContext c, IEnumerable`1 p)
at Autofac.Builder.RegistrationBuilder.<>c__DisplayClass0_0`1.<ForDelegate>b__0(IComponentContext c, IEnumerable`1 p)
at Autofac.Core.Activators.Delegate.DelegateActivator.ActivateInstance(IComponentContext context, IEnumerable`1 parameters)
at Autofac.Core.Activators.Delegate.DelegateActivator.<ConfigurePipeline>b__2_0(ResolveRequestContext ctxt, Action`1 next)
at Autofac.Core.Resolving.Middleware.DelegateMiddleware.Execute(ResolveRequestContext context, Action`1 next)
at Autofac.Core.Resolving.Pipeline.ResolvePipelineBuilder.<>c__DisplayClass14_0.<BuildPipeline>b__1(ResolveRequestContext ctxt)
at Autofac.Core.Resolving.Middleware.DisposalTrackingMiddleware.Execute(ResolveRequestContext context, Action`1 next)
at Autofac.Core.Resolving.Pipeline.ResolvePipelineBuilder.<>c__DisplayClass14_0.<BuildPipeline>b__1(ResolveRequestContext ctxt)
at Autofac.Core.Resolving.Middleware.ActivatorErrorHandlingMiddleware.Execute(ResolveRequestContext context, Action`1 next)
}
Operation FAILED
注意返回到中间件的行程如何变为 X-
?我们知道错误发生在执行 lambda 时。你可以使用这些提示确切地看到问题出在哪里。
DOT 图形追踪器
除了 DefaultDiagnosticTracer
,我们还提供了 Autofac.Diagnostics.DotGraph
包中的图形追踪器。
如果你添加对这个包的引用,你将能够使用 DOT 语言 以视觉方式追踪完整的依赖树。然后,你可以使用像 Graphviz 这样的工具渲染图像。
首先,就像使用 DefaultDiagnosticTracer
一样,将它注册到你的容器中。这次,跟踪输出将是 DOT 图形格式。
// 正常构建你的容器。
var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterType<Component>().As<IService>();
var container = containerBuilder.Build();
// 创建一个 DOT 图形追踪器实例。跟踪内容将是 DOT 图形格式。
var tracer = new DotDiagnosticTracer();
tracer.OperationCompleted += (sender, args) =>
{
// 将 DOT 跟踪写入文件可以让您稍后使用 Graphviz 渲染它,但这不是一个很好的复制/粘贴示例。您应该使用异步方式处理,并带有良好的错误处理。
var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.dot");
using var file = new StreamWriter(path);
file.WriteLine(args.TraceContent);
};
// 使用你的追踪器订阅诊断。
container.SubscribeToDiagnostics(tracer);
// 正常解析。每当有东西被解析时,追踪器事件就会触发。
using var scope = container.BeginLifetimeScope();
scope.Resolve<IService>();
假设你有一个简单的 lambda,它注册一个字符串。
var builder = new ContainerBuilder();
builder.Register(ctx => "HelloWorld");
var container = builder.Build();
DOT 图形追踪器的输出看起来像这样(确实很乱):
digraph G {
label=<string<br/><font point-size="8">Operation #1</font>>;
labelloc=t
na58baa0161f74ca8a74d3481aff7d182 [shape=component,label=<
<table border='0' cellborder='0' cellspacing='0'>
<tr><td port='nb569aeb076c94321a3c17b56bf16fd2c'>string</td></tr>
<tr><td><font point-size="10">Component: λ:string</font></td></tr>
</table>
>];
}
不过,假设你将这些信息保存到文件中,然后用 Graphviz 将其转换成 PNG:
dot -Tpng -O my-trace.dot
输出的图形看起来像这样:
现在看起来有些意思了。我们可以看到这是对一个字符串的解析,并且是由一个 lambda 来完成的。
但是,对于更复杂的场景呢?以下是复杂解析图的一个例子。
从这个图中,我们可以获取很多信息:
- 需要解析
IHandler<string>
和IService1
,它们都需要IService2
,并且使用了一个单例实例来满足需求。这意味着它可能是单例,也可能是每个生命周期范围一个实例。 IService1
和IService2
都需要IService3
,并且每个实例都会创建一个新的IService3
实例。IService3
被装饰了——看它如何向下链接到看起来更像一个盒子的节点。这表明有装饰器在起作用。你可以在这个框中看到组件(装饰器)和目标(被装饰的对象)。IService3
的构造函数参数需要ILifetimeScope
。
最后一个参数 —— ILifetimeScope
—— 意味着 IService3
可能会在代码内部进行服务定位(手动解析)。如果你真的想知道完整的链路,可能需要将这个图与其他图关联起来。但是怎么做呢?
注意顶部有一个 “操作 #1” 计数器——每次通过追踪器的解析操作都会增加这个计数器。你可以查找计数值较大的跟踪,然后做一些手动关联。不幸的是,这就是我们能做到的极限,因为每个解析都是独立的——服务定位会打断链路。你不能假设与生命周期范围关联的所有解析是相关的,例如,可能整个应用程序的所有解析都来自同一个范围。
错误也会被高亮显示,以便你能看到错误发生在哪里。
在这种情况下,你可以看到失败的地方,红色粗体突出显示。你还可以看到异常类型和消息。
自定义追踪器
使用 System.Diagnostics.DiagnosticSource
,Autofac 允许你创建自定义追踪器,处理各种事件并生成你感兴趣的任何数据。
整体管道中的事件按照以下顺序发生:
- 操作开始
- 解析请求开始
- 中间件开始
- 中间件成功/失败
- 解析请求成功/失败
- 解析请求开始
- 操作成功/失败
中间件可能会启动额外的解析请求;而且管道中有多个中间件项。有关更多详细信息,请参阅 管道 页面。
如果你想追踪整个操作,就像 DefaultDiagnosticTracer
一样,可以从 Autofac.Diagnostics.OperationDiagnosticTracerBase<TContent>
类开始。DefaultDiagnosticTracer
就是基于这个类构建的。它有意地监听所有解析事件,从头到尾,一次跟踪一个完整操作。你最好的例子是查看 DefaultDiagnosticTracer
的 源代码 。由于要处理的事件很多,需要捕获的数据也很多。
你可以稍微控制一些,只追踪某些事件,使用 Autofac.Diagnostics.DiagnosticTracerBase
。这是一个 DiagnosticListener
,它为事件添加了一些强类型解析,帮助你编写较少的代码。以下是一个在解析操作开始时将日志写入控制台的追踪器示例:
下面是一个追踪完整操作并只保留类似 DefaultDiagnosticTracer
的简单数据堆栈的示例,但没有那么花哨。
public class ConsoleOperationTracer : DiagnosticTracerBase
{
public ConsoleOperationTracer()
: base()
{
EnableBase("Autofac.Operation.Start");
}
protected override void OnOperationStart(OperationStartDiagnosticData data)
{
Console.WriteLine("Operation starting.");
}
}
现在你可以使用你的自定义追踪器。它不会引发任何事件,但会记录你想要的内容。
// 正常方式构建容器。
var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterType<Component>().As<IService>();
var container = containerBuilder.Build();
// 使用你的追踪器订阅诊断。
container.SubscribeToDiagnostics<ConsoleOperationTracer>();
如果你想要更大的控制权,你可以利用 System.Diagnostics.DiagnosticListener
默认使用的 IObserver<KeyValuePair<string, object>>
支持。以下是同样的控制台日志监听器的这种格式:
public class ConsoleOperationTracer : IObserver<KeyValuePair<string, object>>
{
public void OnCompleted()
{
}
public void OnError(Exception error)
{
}
public void OnNext(KeyValuePair<string, object> value)
{
// 追踪器只会根据我们如何注册它来调用,因此在操作开始时。
//
// 当操作开始事件被触发时,value.Value将是OperationStartDiagnosticData,但这个日志器不使用它。
Console.WriteLine("Operation starting.");
}
}
如你所见,如果你深入底层,可以编写非常紧密、性能良好的代码。
当你达到这个程度时,你可以独立于追踪器控制事件订阅。你必须直接将追踪器注册到容器的 DiagnosticSource
。
// 使用你的追踪器订阅诊断。
// 注意Lambda表达式,用于告诉追踪器是否应该接收到事件。
var tracer = new ConsoleOperationTracer();
container.DiagnosticSource.Subscribe(tracer, e => e == "Autofac.Operation.Start");
符号和源代码
Autofac 包已更新以使用 Source Link ,以便你可以直接从代码中调试到 Autofac 源代码。包可能包含符号直接在里面,也可能在 NuGet 符号服务器 中。
在 Visual Studio 中,有启用搜索 NuGet 符号服务器的选项。请参阅微软文档,了解如何配置 Visual Studio 以使符号服务器工作。
在 VS Code 中,你可能需要在 settings.json
或 launch.json
中设置调试选项。
要在单元测试调试中启用符号,settings.json
的块看起来像这样:
{
"csharp.unitTestDebuggingOptions": {
"symbolOptions": {
"searchMicrosoftSymbolServer": true,
"searchNuGetOrgSymbolServer": true
}
}
}
要使用符号启动应用程序,launch.json
可能看起来像这样:
{
"configurations": [
{
"console": "internalConsole",
"cwd": "${workspaceFolder}/src/MyProject",
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "https://localhost:5000",
"COMPlus_ReadyToRun": "0",
"COMPlus_ZapDisable": "1"
},
"justMyCode": false,
"name": "使用SourceLink(开发)启动",
"preLaunchTask": "build",
"program": "${workspaceFolder}/src/MyProject/bin/Debug/net6.0/MyProject.dll",
"request": "launch",
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)",
"uriFormat": "%s"
},
"stopAtEntry": false,
"suppressJITOptimizations": true,
"symbolOptions": {
"searchMicrosoftSymbolServer": true,
"searchNuGetOrgSymbolServer": true
},
"type": "coreclr"
}
],
"version": "0.2.0"
}