5

I recently started working for an organization that has an enormous code base that uses Autofac as their DI Container of choice.

Unfortunately, not all developers follow best practices when it comes to writing their classes, which means they sometimes call external services and do other heavy lifting inside their constructors. This is a DI code smell, as injection constructors should be simple.

Considering the size of the application, and the number of developers working on the code, it is not feasible to review all existing classes one by one. Instead I would like to write an integration test that hooks into the Autofac pipeline and reports resolves that take a considerate amount of time.

How would I achieve such a thing? What interception points does Autofac expose that allows this measuring to take place?

With Simple Injector, I'm able to achieve this by registering a resolve interceptor, as shown in this example:

container.Options.RegisterResolveInterceptor((context, producer) =>
    {
        var watch = Stopwatch.StartNew();
        try
        {
            return producer.Invoke();
        }
        finally
        {
            if (watch.TotalElapsedMiliseconds > THRESHOLD)
            {
                Console.WriteLine(
                    $"Resolving {context.Registration.ImplementationType} " +
                    $"took {watch.TotalElapsedMiliseconds} ms.");
            }
        }
    },
    c => true);

How to achieve a similar result with Autofac?

Nkosi
  • 235,767
  • 35
  • 427
  • 472
Steven
  • 166,672
  • 24
  • 332
  • 435
  • Does this help? https://stackoverflow.com/a/23393387/1663001 – DavidG Jun 04 '19 at 10:47
  • The first line, however, does answer my question. I'm still thinking about whether or not this would be a duplicate or not. Since my answer is about measuring time, I'm considering leaving it open, so I can answer the question myself with my final solution. – Steven Jun 04 '19 at 11:44
  • @DavidG, looking at your suggested link, I guess you can subscribe to the events and map the instance being resolved in a dictionary with a stop watch – Nkosi Jun 04 '19 at 11:45
  • @Nkosi: That's exactly what I'm currently trying right now :) – Steven Jun 04 '19 at 11:46
  • I had a try and couldn't get it working, there's no post-resolve event that I can see? – DavidG Jun 04 '19 at 11:57
  • `Preparing` is pre-resolution and `Activating` in post-resolution. During `Activating`, the component has already been created. – Steven Jun 04 '19 at 12:03
  • Never mind, my test code was nonsense. I do wonder if this is thread safe, is the module created for every activation? – DavidG Jun 04 '19 at 12:07
  • @DavidG: A module is created just once. I currently build an object graph and store it in `ThreadLocal`. – Steven Jun 04 '19 at 12:15
  • How does `ThreadLocal` help? – DavidG Jun 04 '19 at 12:17
  • @DavidG It would help because the container can resolve graphs over multiple threads. In practice, however, I would run this on a single thread inside an integration test, so multi-threading won't be an issue. – Steven Jun 04 '19 at 12:20
  • I should clarify. Let's say you have `Foo` that takes a `Bar` in it's ctor, how can you measure the creation time of `Foo` and `Bar` individually? The module will prepare `Foo` first, then realise it needs to prepare `Bar`. – DavidG Jun 04 '19 at 12:20
  • @DavidG: See my answer. – Steven Jun 04 '19 at 12:33

1 Answers1

5

Using an Autofac Module, you can hook onto the Preparing and Activating events:

Autofac registration:

builder.RegisterModule<TimeMeasuringResolveModule>();

TimeMeasuringResolveModule:

public class TimeMeasuringResolveModule : Module
{
    private ResolveInfo _current;

    protected override void AttachToComponentRegistration(
        IComponentRegistry componentRegistry, IComponentRegistration registration)
    {
        registration.Preparing += Registration_Preparing;
        registration.Activating += Registration_Activating;

        base.AttachToComponentRegistration(componentRegistry, registration);
    }

    private void Registration_Preparing(object sender, PreparingEventArgs e)
    {
        // Called before resolving type
        _current = new ResolveInfo(e.Component.Activator.LimitType, _current);
    }

    private void Registration_Activating(object sender, ActivatingEventArgs<object> e)
    {
        // Called when type is constructed
        var current = _current;
        current.MarkComponentAsResolved();
        _current = current.Parent;

        if (current.Parent == null)
        {
            ResolveInfoVisualizer.VisualizeGraph(current);
        }
    }
}

ResolveInfo:

public sealed class ResolveInfo
{
    private Stopwatch _watch = Stopwatch.StartNew();

    public ResolveInfo(Type componentType, ResolveInfo parent)
    {
        ComponentType = componentType;
        Parent = parent;
        Dependencies = new List<ResolveInfo>(4);

        if (parent != null) parent.Dependencies.Add(this);
    }

    public Type ComponentType { get; }
    public List<ResolveInfo> Dependencies { get; }

    // Time it took to create the type including its dependencies
    public TimeSpan ResolveTime { get; private set; }

    // Time it took to create the type excluding its dependencies
    public TimeSpan CreationTime { get; private set; }
    public ResolveInfo Parent { get; }

    public void MarkComponentAsResolved()
    {
        ResolveTime = _watch.Elapsed;
        CreationTime = ResolveTime;

        foreach (var dependency in this.Dependencies)
        {
            CreationTime -= dependency.ResolveTime;
        }
    }
}

ResolveInfoVisualizer:

public static class ResolveInfoVisualizer
{
    public static void VisualizeGraph(ResolveInfo node, int depth = 0)
    {
        Debug.WriteLine(
            $"{new string(' ', depth * 3)}" +
            $"{node.ComponentType.FullName} " +
            $"({node.ResolveTime.TotalMilliseconds.ToString("F1")} ms. / " +
            $"/ {node.CreationTime.TotalMilliseconds.ToString("F1")} ms.)");

        foreach (var dependency in node.Dependencies)
        {
            VisualizeGraph(dependency, depth + 1);
        }
    }
}

Instead of logging to the Debug Window, you should typically use the output in a unit test.

WARNING: The TimeMeasuringResolveModule is NOT thread-safe. Considering the performance overhead of this module, you should only use it as part of a single integration test.

Also note that although this module does generate object graphs, it does not output a representive object graph, but only consists of the objects that are actually activated during that resolve. Already initialized singletons, for instance, will not show up in the graph, as they are effectively no-ops. For visualizing truthful object graphs, a different method should be used.

Steven
  • 166,672
  • 24
  • 332
  • 435
  • I see, very interesting. Though the last comment about it not being thread safe is a worry, especially if this is an ASP.NET app. – DavidG Jun 04 '19 at 12:41
  • 1
    I would suggest only adding this module to Autofac to a single integration test and never run in production as there is quite some overhead. – Steven Jun 04 '19 at 13:24
  • @Steven while overly simplified, take a look at this version of your implementation on .net fiddler https://dotnetfiddle.net/oPcDGG – Nkosi Jun 04 '19 at 14:17