We have a huge Visual Studio solution and a lot of developers and to measure how long local builds run in average (and which projects take the most time) over all developers, we wanted to create a Visual Studio extension which always copies the output of the window “Build Order” after a successful build to a specific folder, so we are able to analyze the data later on.

Here’s the code of our AsyncPackage:

[PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
[ProvideAutoLoad(UIContextGuids80.NoSolution, PackageAutoLoadFlags.BackgroundLoad)]
[ProvideAutoLoad(UIContextGuids80.SolutionExists, PackageAutoLoadFlags.BackgroundLoad)]
public sealed class BuildOutputLoggerPackage : AsyncPackage
    public const string PACKAGE_GUID_STRING = "XXX";
    private DTE2 _dte2;

    protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
        // When initialized asynchronously, the current thread may be a background thread at this point.
        // Do any initialization that requires the UI thread after switching to the UI thread.
        await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

        _dte2 = (await GetServiceAsync(typeof(DTE))) as DTE2;
        if (_dte2 == null)

        // Keep BuildEvents reference to prevent GC from collecting it and prevent creating multiple COM-objects.
        // Because every time we access BuildEvents property, a new COM-object is created.
        var buildEvents = _dte2.Events.BuildEvents;
        buildEvents.OnBuildDone += BuildEvents_OnBuildDone;

    private void BuildEvents_OnBuildDone(vsBuildScope scope, vsBuildAction action)
        if (scope != vsBuildScope.vsBuildScopeSolution)

        if (action != vsBuildAction.vsBuildActionBuild && action != vsBuildAction.vsBuildActionRebuildAll)

        string text = null;
        ThreadHelper.JoinableTaskFactory.Run(async () =>
            await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

            // This indicates a failed build
            // https://learn.microsoft.com/de-de/dotnet/api/envdte.solutionbuild.lastbuildinfo?view=visualstudiosdk-2022
            if (_dte2.Solution.SolutionBuild.LastBuildInfo > 0)

            var sortedBuildOutputPane = _dte2.ToolWindows.OutputWindow.OutputWindowPanes
                .FirstOrDefault(p =>
#pragma warning disable VSTHRD010 // Invoke single-threaded types on Main thread
                    return p.Guid == VSConstants.OutputWindowPaneGuid.SortedBuildOutputPane_string;
#pragma warning restore VSTHRD010 // Invoke single-threaded types on Main thread

            if (sortedBuildOutputPane == null)

            var start = sortedBuildOutputPane.TextDocument.StartPoint.CreateEditPoint();
            text = start.GetText(sortedBuildOutputPane.TextDocument.EndPoint);

        if (string.IsNullOrEmpty(text))

        var info = CreateBuildOutputInfo(text);

    private BuildOutputInfo CreateBuildOutputInfo(string output)
        return new BuildOutputInfo()
            ExtensionVersion = typeof(BuildOutputLoggerPackage).Assembly.GetName().Version,
            User = _cachedUserInfo,
            System = _cachedSystemInfo,
            TFVC = _tfvcInfo,
            Output = output

If a developer starts Visual Studio and opens a solution inside of it, it works as intended, because my extension gets loaded at or right after the start of Visual Studio.

The problem occurs, when somebody double clicks our huge solution in Windows Explorer, because then Visual Studio doesn’t seem to have enough time to load my extension. The developer runs a build after the solution finished loading, the loading of my extension gets queued and only after the build has finished, my extension finally gets loaded. Then it’s too late.

Is there a way I can force Visual Studio to load my extension right at the start consistently?

I know an extension called “VSColorOutput” which uses a text classifier at it seems to doesn’t have the problem I have. I thought about adding a “dummy text classifier” to my extension, but that seems a bit hacky. Not my style.