项目
版本

检查

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

输出的图形看起来像这样:

字符串解析的简单DOT图

现在看起来有些意思了。我们可以看到这是对一个字符串的解析,并且是由一个 lambda 来完成的。

但是,对于更复杂的场景呢?以下是复杂解析图的一个例子。

复杂解析的DOT图

从这个图中,我们可以获取很多信息:

  • 需要解析 IHandler<string>IService1 ,它们都需要 IService2 ,并且使用了一个单例实例来满足需求。这意味着它可能是单例,也可能是每个生命周期范围一个实例。
  • IService1IService2 都需要 IService3 ,并且每个实例都会创建一个新的 IService3 实例。
  • IService3 被装饰了——看它如何向下链接到看起来更像一个盒子的节点。这表明有装饰器在起作用。你可以在这个框中看到组件(装饰器)和目标(被装饰的对象)。
  • IService3 的构造函数参数需要 ILifetimeScope

最后一个参数 —— ILifetimeScope —— 意味着 IService3 可能会在代码内部进行服务定位(手动解析)。如果你真的想知道完整的链路,可能需要将这个图与其他图关联起来。但是怎么做呢?

注意顶部有一个 “操作 #1” 计数器——每次通过追踪器的解析操作都会增加这个计数器。你可以查找计数值较大的跟踪,然后做一些手动关联。不幸的是,这就是我们能做到的极限,因为每个解析都是独立的——服务定位会打断链路。你不能假设与生命周期范围关联的所有解析是相关的,例如,可能整个应用程序的所有解析都来自同一个范围。

错误也会被高亮显示,以便你能看到错误发生在哪里。

解析期间出现错误的DOT图

在这种情况下,你可以看到失败的地方,红色粗体突出显示。你还可以看到异常类型和消息。

自定义追踪器

使用 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.jsonlaunch.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"
}
在本文档中