Additional Diagnostics in Dependency Injection (#56809)
authorEric Erhardt <eric.erhardt@microsoft.com>
Sat, 7 Aug 2021 15:21:54 +0000 (10:21 -0500)
committerGitHub <noreply@github.com>
Sat, 7 Aug 2021 15:21:54 +0000 (10:21 -0500)
* Additional Diagnostics in Dependency Injection

Log events when a ServiceProvider is created:

* How many singleton, scoped, transient services?
* Log the list of registrations

Fix #56313

* Add ServiceProvider HashCode to all events.

* Write ServiceProvider information when DependencyInjectionEventSource becomes enabled.

This allows for listeners to attach to a process after it is running, and get the DI information.

* Update new events to use Informational level and to have a Keyword.

* Switch to use WeakReference when holding on to ServiceProviders in DependencyInjectionEventSource.

src/libraries/Microsoft.Extensions.DependencyInjection/src/DependencyInjectionEventSource.cs
src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs
src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/DynamicServiceProviderEngine.cs
src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/Expressions/ExpressionResolverBuilder.cs
src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ILEmit/ILEmitResolverBuilder.cs
src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs
src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/DependencyInjectionEventSourceTests.cs

index 32f7596..338325e 100644 (file)
@@ -2,9 +2,11 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
+using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.Diagnostics.Tracing;
 using System.Linq.Expressions;
+using System.Text;
 using Microsoft.Extensions.DependencyInjection.ServiceLookup;
 
 namespace Microsoft.Extensions.DependencyInjection
@@ -14,9 +16,16 @@ namespace Microsoft.Extensions.DependencyInjection
     {
         public static readonly DependencyInjectionEventSource Log = new DependencyInjectionEventSource();
 
-        // Event source doesn't support large payloads so we chunk formatted call site tree
+        public static class Keywords
+        {
+            public const EventKeywords ServiceProviderInitialized = (EventKeywords)0x1;
+        }
+
+        // Event source doesn't support large payloads so we chunk large payloads like formatted call site tree and descriptors
         private const int MaxChunkSize = 10 * 1024;
 
+        private readonly List<WeakReference<ServiceProvider>> _providers = new();
+
         private DependencyInjectionEventSource() : base(EventSourceSettings.EtwSelfDescribingEventFormat)
         {
         }
@@ -32,27 +41,27 @@ namespace Microsoft.Extensions.DependencyInjection
         [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
             Justification = "Parameters to this method are primitive and are trimmer safe.")]
         [Event(1, Level = EventLevel.Verbose)]
-        private void CallSiteBuilt(string serviceType, string callSite, int chunkIndex, int chunkCount)
+        private void CallSiteBuilt(string serviceType, string callSite, int chunkIndex, int chunkCount, int serviceProviderHashCode)
         {
-            WriteEvent(1, serviceType, callSite, chunkIndex, chunkCount);
+            WriteEvent(1, serviceType, callSite, chunkIndex, chunkCount, serviceProviderHashCode);
         }
 
         [Event(2, Level = EventLevel.Verbose)]
-        public void ServiceResolved(string serviceType)
+        public void ServiceResolved(string serviceType, int serviceProviderHashCode)
         {
-            WriteEvent(2, serviceType);
+            WriteEvent(2, serviceType, serviceProviderHashCode);
         }
 
         [Event(3, Level = EventLevel.Verbose)]
-        public void ExpressionTreeGenerated(string serviceType, int nodeCount)
+        public void ExpressionTreeGenerated(string serviceType, int nodeCount, int serviceProviderHashCode)
         {
-            WriteEvent(3, serviceType, nodeCount);
+            WriteEvent(3, serviceType, nodeCount, serviceProviderHashCode);
         }
 
         [Event(4, Level = EventLevel.Verbose)]
-        public void DynamicMethodBuilt(string serviceType, int methodSize)
+        public void DynamicMethodBuilt(string serviceType, int methodSize, int serviceProviderHashCode)
         {
-            WriteEvent(4, serviceType, methodSize);
+            WriteEvent(4, serviceType, methodSize, serviceProviderHashCode);
         }
 
         [Event(5, Level = EventLevel.Verbose)]
@@ -62,52 +71,206 @@ namespace Microsoft.Extensions.DependencyInjection
         }
 
         [Event(6, Level = EventLevel.Error)]
-        public void ServiceRealizationFailed(string? exceptionMessage)
+        public void ServiceRealizationFailed(string? exceptionMessage, int serviceProviderHashCode)
+        {
+            WriteEvent(6, exceptionMessage, serviceProviderHashCode);
+        }
+
+        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
+            Justification = "Parameters to this method are primitive and are trimmer safe.")]
+        [Event(7, Level = EventLevel.Informational, Keywords = Keywords.ServiceProviderInitialized)]
+        private void ServiceProviderBuilt(int serviceProviderHashCode, int singletonServices, int scopedServices, int transientServices)
         {
-            WriteEvent(6, exceptionMessage);
+            WriteEvent(7, serviceProviderHashCode, singletonServices, scopedServices, transientServices);
+        }
+
+        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
+            Justification = "Parameters to this method are primitive and are trimmer safe.")]
+        [Event(8, Level = EventLevel.Informational, Keywords = Keywords.ServiceProviderInitialized)]
+        private void ServiceProviderDescriptors(int serviceProviderHashCode, string descriptors, int chunkIndex, int chunkCount)
+        {
+            WriteEvent(8, serviceProviderHashCode, descriptors, chunkIndex, chunkCount);
         }
 
         [NonEvent]
-        public void ServiceResolved(Type serviceType)
+        public void ServiceResolved(ServiceProvider provider, Type serviceType)
         {
             if (IsEnabled(EventLevel.Verbose, EventKeywords.All))
             {
-                ServiceResolved(serviceType.ToString());
+                ServiceResolved(serviceType.ToString(), provider.GetHashCode());
             }
         }
 
         [NonEvent]
-        public void CallSiteBuilt(Type serviceType, ServiceCallSite callSite)
+        public void CallSiteBuilt(ServiceProvider provider, Type serviceType, ServiceCallSite callSite)
         {
             if (IsEnabled(EventLevel.Verbose, EventKeywords.All))
             {
                 string format = CallSiteJsonFormatter.Instance.Format(callSite);
                 int chunkCount = format.Length / MaxChunkSize + (format.Length % MaxChunkSize > 0 ? 1 : 0);
 
+                int providerHashCode = provider.GetHashCode();
                 for (int i = 0; i < chunkCount; i++)
                 {
                     CallSiteBuilt(
                         serviceType.ToString(),
-                        format.Substring(i * MaxChunkSize, Math.Min(MaxChunkSize, format.Length - i * MaxChunkSize)), i, chunkCount);
+                        format.Substring(i * MaxChunkSize, Math.Min(MaxChunkSize, format.Length - i * MaxChunkSize)), i, chunkCount,
+                        providerHashCode);
                 }
             }
         }
 
         [NonEvent]
-        public void DynamicMethodBuilt(Type serviceType, int methodSize)
+        public void DynamicMethodBuilt(ServiceProvider provider, Type serviceType, int methodSize)
         {
             if (IsEnabled(EventLevel.Verbose, EventKeywords.All))
             {
-                DynamicMethodBuilt(serviceType.ToString(), methodSize);
+                DynamicMethodBuilt(serviceType.ToString(), methodSize, provider.GetHashCode());
             }
         }
 
         [NonEvent]
-        public void ServiceRealizationFailed(Exception exception)
+        public void ServiceRealizationFailed(Exception exception, int serviceProviderHashCode)
         {
             if (IsEnabled(EventLevel.Error, EventKeywords.All))
             {
-                ServiceRealizationFailed(exception.ToString());
+                ServiceRealizationFailed(exception.ToString(), serviceProviderHashCode);
+            }
+        }
+
+        [NonEvent]
+        public void ServiceProviderBuilt(ServiceProvider provider)
+        {
+            lock (_providers)
+            {
+                _providers.Add(new WeakReference<ServiceProvider>(provider));
+            }
+
+            WriteServiceProviderBuilt(provider);
+        }
+
+        [NonEvent]
+        public void ServiceProviderDisposed(ServiceProvider provider)
+        {
+            lock (_providers)
+            {
+                for (int i = _providers.Count - 1; i >= 0; i--)
+                {
+                    // remove the provider, along with any stale references
+                    WeakReference<ServiceProvider> reference = _providers[i];
+                    if (!reference.TryGetTarget(out ServiceProvider target) || target == provider)
+                    {
+                        _providers.RemoveAt(i);
+                    }
+                }
+            }
+        }
+
+        [NonEvent]
+        private void WriteServiceProviderBuilt(ServiceProvider provider)
+        {
+            if (IsEnabled(EventLevel.Informational, Keywords.ServiceProviderInitialized))
+            {
+                int singletonServices = 0;
+                int scopedServices = 0;
+                int transientServices = 0;
+
+                StringBuilder descriptorBuilder = new StringBuilder("{ \"descriptors\":[ ");
+                bool firstDescriptor = true;
+                foreach (ServiceDescriptor descriptor in provider.CallSiteFactory.Descriptors)
+                {
+                    if (firstDescriptor)
+                    {
+                        firstDescriptor = false;
+                    }
+                    else
+                    {
+                        descriptorBuilder.Append(", ");
+                    }
+
+                    AppendServiceDescriptor(descriptorBuilder, descriptor);
+
+                    switch (descriptor.Lifetime)
+                    {
+                        case ServiceLifetime.Singleton:
+                            singletonServices++;
+                            break;
+                        case ServiceLifetime.Scoped:
+                            scopedServices++;
+                            break;
+                        case ServiceLifetime.Transient:
+                            transientServices++;
+                            break;
+                    }
+                }
+                descriptorBuilder.Append(" ] }");
+
+                int providerHashCode = provider.GetHashCode();
+                ServiceProviderBuilt(providerHashCode, singletonServices, scopedServices, transientServices);
+
+                string descriptorString = descriptorBuilder.ToString();
+                int chunkCount = descriptorString.Length / MaxChunkSize + (descriptorString.Length % MaxChunkSize > 0 ? 1 : 0);
+
+                for (int i = 0; i < chunkCount; i++)
+                {
+                    ServiceProviderDescriptors(
+                        providerHashCode,
+                        descriptorString.Substring(i * MaxChunkSize, Math.Min(MaxChunkSize, descriptorString.Length - i * MaxChunkSize)), i, chunkCount);
+                }
+            }
+        }
+
+        [NonEvent]
+        private static void AppendServiceDescriptor(StringBuilder builder, ServiceDescriptor descriptor)
+        {
+            builder.Append("{ \"serviceType\": \"");
+            builder.Append(descriptor.ServiceType);
+            builder.Append("\", \"lifetime\": \"");
+            builder.Append(descriptor.Lifetime);
+            builder.Append("\", ");
+
+            if (descriptor.ImplementationType is not null)
+            {
+                builder.Append("\"implementationType\": \"");
+                builder.Append(descriptor.ImplementationType);
+            }
+            else if (descriptor.ImplementationFactory is not null)
+            {
+                builder.Append("\"implementationFactory\": \"");
+                builder.Append(descriptor.ImplementationFactory.Method);
+            }
+            else if (descriptor.ImplementationInstance is not null)
+            {
+                builder.Append("\"implementationInstance\": \"");
+                builder.Append(descriptor.ImplementationInstance.GetType());
+                builder.Append(" (instance)");
+            }
+            else
+            {
+                builder.Append("\"unknown\": \"");
+            }
+
+            builder.Append("\" }");
+        }
+
+        protected override void OnEventCommand(EventCommandEventArgs command)
+        {
+            if (command.Command == EventCommand.Enable)
+            {
+                // When this EventSource becomes enabled, write out the existing ServiceProvider information
+                // because building the ServiceProvider happens early in the process. This way a listener
+                // can get this information, even if they attach while the process is running.
+
+                lock (_providers)
+                {
+                    foreach (WeakReference<ServiceProvider> reference in _providers)
+                    {
+                        if (reference.TryGetTarget(out ServiceProvider provider))
+                        {
+                            WriteServiceProviderBuilt(provider);
+                        }
+                    }
+                }
             }
         }
     }
@@ -117,13 +280,13 @@ namespace Microsoft.Extensions.DependencyInjection
         // This is an extension method because this assembly is trimmed at a "type granular" level in Blazor,
         // and the whole DependencyInjectionEventSource type can't be trimmed. So extracting this to a separate
         // type allows for the System.Linq.Expressions usage to be trimmed by the ILLinker.
-        public static void ExpressionTreeGenerated(this DependencyInjectionEventSource source, Type serviceType, Expression expression)
+        public static void ExpressionTreeGenerated(this DependencyInjectionEventSource source, ServiceProvider provider, Type serviceType, Expression expression)
         {
             if (source.IsEnabled(EventLevel.Verbose, EventKeywords.All))
             {
                 var visitor = new NodeCountingVisitor();
                 visitor.Visit(expression);
-                source.ExpressionTreeGenerated(serviceType.ToString(), visitor.NodeCount);
+                source.ExpressionTreeGenerated(serviceType.ToString(), visitor.NodeCount, provider.GetHashCode());
             }
         }
 
index 02db795..36e0ab4 100644 (file)
@@ -30,6 +30,8 @@ namespace Microsoft.Extensions.DependencyInjection.ServiceLookup
             Populate();
         }
 
+        internal ServiceDescriptor[] Descriptors => _descriptors;
+
         private void Populate()
         {
             foreach (ServiceDescriptor descriptor in _descriptors)
index 0237e8a..09abe62 100644 (file)
@@ -37,7 +37,7 @@ namespace Microsoft.Extensions.DependencyInjection.ServiceLookup
                         }
                         catch (Exception ex)
                         {
-                            DependencyInjectionEventSource.Log.ServiceRealizationFailed(ex);
+                            DependencyInjectionEventSource.Log.ServiceRealizationFailed(ex, _serviceProvider.GetHashCode());
 
                             Debug.Fail($"We should never get exceptions from the background compilation.{Environment.NewLine}{ex}");
                         }
index a74a004..e72553e 100644 (file)
@@ -68,7 +68,7 @@ namespace Microsoft.Extensions.DependencyInjection.ServiceLookup
         public Func<ServiceProviderEngineScope, object> BuildNoCache(ServiceCallSite callSite)
         {
             Expression<Func<ServiceProviderEngineScope, object>> expression = BuildExpression(callSite);
-            DependencyInjectionEventSource.Log.ExpressionTreeGenerated(callSite.ServiceType, expression);
+            DependencyInjectionEventSource.Log.ExpressionTreeGenerated(_rootScope.RootProvider, callSite.ServiceType, expression);
             return expression.Compile();
         }
 
index aeb27d9..878ba33 100644 (file)
@@ -112,7 +112,7 @@ namespace Microsoft.Extensions.DependencyInjection.ServiceLookup
             type.CreateTypeInfo();
             assembly.Save(assemblyName + ".dll");
 #endif
-            DependencyInjectionEventSource.Log.DynamicMethodBuilt(callSite.ServiceType, ilGenerator.ILOffset);
+            DependencyInjectionEventSource.Log.DynamicMethodBuilt(_rootScope.RootProvider, callSite.ServiceType, ilGenerator.ILOffset);
 
             return new GeneratedMethod()
             {
index 9e3698f..871eb4b 100644 (file)
@@ -75,6 +75,7 @@ namespace Microsoft.Extensions.DependencyInjection
                 }
             }
 
+            DependencyInjectionEventSource.Log.ServiceProviderBuilt(this);
         }
 
         /// <summary>
@@ -87,17 +88,23 @@ namespace Microsoft.Extensions.DependencyInjection
         /// <inheritdoc />
         public void Dispose()
         {
-            _disposed = true;
+            DisposeCore();
             Root.Dispose();
         }
 
         /// <inheritdoc/>
         public ValueTask DisposeAsync()
         {
-            _disposed = true;
+            DisposeCore();
             return Root.DisposeAsync();
         }
 
+        private void DisposeCore()
+        {
+            _disposed = true;
+            DependencyInjectionEventSource.Log.ServiceProviderDisposed(this);
+        }
+
         private void OnCreate(ServiceCallSite callSite)
         {
             _callSiteValidator?.ValidateCallSite(callSite);
@@ -117,7 +124,7 @@ namespace Microsoft.Extensions.DependencyInjection
 
             Func<ServiceProviderEngineScope, object> realizedService = _realizedServices.GetOrAdd(serviceType, _createServiceAccessor);
             OnResolve(serviceType, serviceProviderEngineScope);
-            DependencyInjectionEventSource.Log.ServiceResolved(serviceType);
+            DependencyInjectionEventSource.Log.ServiceResolved(this, serviceType);
             var result = realizedService.Invoke(serviceProviderEngineScope);
             System.Diagnostics.Debug.Assert(result is null || CallSiteFactory.IsService(serviceType));
             return result;
@@ -149,7 +156,7 @@ namespace Microsoft.Extensions.DependencyInjection
             ServiceCallSite callSite = CallSiteFactory.GetCallSite(serviceType, new CallSiteChain());
             if (callSite != null)
             {
-                DependencyInjectionEventSource.Log.CallSiteBuilt(serviceType, callSite);
+                DependencyInjectionEventSource.Log.CallSiteBuilt(this, serviceType, callSite);
                 OnCreate(callSite);
 
                 // Optimize singleton case
index 54a282d..11d8ae9 100644 (file)
@@ -6,6 +6,7 @@ using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Diagnostics.Tracing;
 using System.Linq;
+using System.Reflection;
 using Microsoft.Extensions.DependencyInjection.Specification.Fakes;
 using Newtonsoft.Json.Linq;
 using Xunit;
@@ -18,12 +19,16 @@ namespace Microsoft.Extensions.DependencyInjection.Tests
     }
 
     [Collection(nameof(EventSourceTests))]
-    public class DependencyInjectionEventSourceTests: IDisposable
+    public class DependencyInjectionEventSourceTests : IDisposable
     {
         private readonly TestEventListener _listener = new TestEventListener();
 
         public DependencyInjectionEventSourceTests()
         {
+            // clear the provider list in between tests
+            typeof(DependencyInjectionEventSource).GetField("_providers", BindingFlags.NonPublic | BindingFlags.Instance)
+                .SetValue(DependencyInjectionEventSource.Log, new List<WeakReference<ServiceProvider>>());
+
             _listener.EnableEvents(DependencyInjectionEventSource.Log, EventLevel.Verbose);
         }
 
@@ -214,15 +219,114 @@ namespace Microsoft.Extensions.DependencyInjection.Tests
         public void EmitsServiceRealizationFailedEvent()
         {
             var exception = new Exception("Test error.");
-            DependencyInjectionEventSource.Log.ServiceRealizationFailed(exception);
+            DependencyInjectionEventSource.Log.ServiceRealizationFailed(exception, 1234);
 
             var eventName = nameof(DependencyInjectionEventSource.Log.ServiceRealizationFailed);
             var serviceRealizationFailedEvent = _listener.EventData.Single(e => e.EventName == eventName);
 
             Assert.Equal("System.Exception: Test error.", GetProperty<string>(serviceRealizationFailedEvent, "exceptionMessage"));
+            Assert.Equal(1234, GetProperty<int>(serviceRealizationFailedEvent, "serviceProviderHashCode"));
             Assert.Equal(6, serviceRealizationFailedEvent.EventId);
         }
 
+        [Fact]
+        public void EmitsServiceProviderBuilt()
+        {
+            ServiceCollection serviceCollection = new();
+            FakeDisposeCallback fakeDisposeCallback = new();
+            serviceCollection.AddSingleton(fakeDisposeCallback);
+            serviceCollection.AddTransient<IFakeOuterService, FakeDisposableCallbackOuterService>();
+            serviceCollection.AddSingleton<IFakeMultipleService, FakeDisposableCallbackInnerService>();
+            serviceCollection.AddSingleton<IFakeMultipleService>(provider => new FakeDisposableCallbackInnerService(fakeDisposeCallback));
+            serviceCollection.AddScoped<IFakeMultipleService, FakeDisposableCallbackInnerService>();
+            serviceCollection.AddTransient<IFakeMultipleService, FakeDisposableCallbackInnerService>();
+            serviceCollection.AddSingleton<IFakeService, FakeDisposableCallbackInnerService>();
+
+            using ServiceProvider provider = serviceCollection.BuildServiceProvider();
+
+            EventWrittenEventArgs serviceProviderBuiltEvent = _listener.EventData.Single(e => e.EventName == "ServiceProviderBuilt");
+            GetProperty<int>(serviceProviderBuiltEvent, "serviceProviderHashCode"); // assert hashcode exists as an int
+            Assert.Equal(4, GetProperty<int>(serviceProviderBuiltEvent, "singletonServices"));
+            Assert.Equal(1, GetProperty<int>(serviceProviderBuiltEvent, "scopedServices"));
+            Assert.Equal(2, GetProperty<int>(serviceProviderBuiltEvent, "transientServices"));
+            Assert.Equal(7, serviceProviderBuiltEvent.EventId);
+
+            EventWrittenEventArgs serviceProviderDescriptorsEvent = _listener.EventData.Single(e => e.EventName == "ServiceProviderDescriptors");
+            Assert.Equal(
+                string.Join(Environment.NewLine,
+                "{",
+                "  \"descriptors\": [",
+                "    {",
+                "      \"serviceType\": \"Microsoft.Extensions.DependencyInjection.Specification.Fakes.FakeDisposeCallback\",",
+                "      \"lifetime\": \"Singleton\",",
+                "      \"implementationInstance\": \"Microsoft.Extensions.DependencyInjection.Specification.Fakes.FakeDisposeCallback (instance)\"",
+                "    },",
+                "    {",
+                "      \"serviceType\": \"Microsoft.Extensions.DependencyInjection.Specification.Fakes.IFakeOuterService\",",
+                "      \"lifetime\": \"Transient\",",
+                "      \"implementationType\": \"Microsoft.Extensions.DependencyInjection.Specification.Fakes.FakeDisposableCallbackOuterService\"",
+                "    },",
+                "    {",
+                "      \"serviceType\": \"Microsoft.Extensions.DependencyInjection.Specification.Fakes.IFakeMultipleService\",",
+                "      \"lifetime\": \"Singleton\",",
+                "      \"implementationType\": \"Microsoft.Extensions.DependencyInjection.Specification.Fakes.FakeDisposableCallbackInnerService\"",
+                "    },",
+                "    {",
+                "      \"serviceType\": \"Microsoft.Extensions.DependencyInjection.Specification.Fakes.IFakeMultipleService\",",
+                "      \"lifetime\": \"Singleton\",",
+                "      \"implementationFactory\": \"Microsoft.Extensions.DependencyInjection.Specification.Fakes.IFakeMultipleService <EmitsServiceProviderBuilt>b__0(System.IServiceProvider)\"",
+                "    },",
+                "    {",
+                "      \"serviceType\": \"Microsoft.Extensions.DependencyInjection.Specification.Fakes.IFakeMultipleService\",",
+                "      \"lifetime\": \"Scoped\",",
+                "      \"implementationType\": \"Microsoft.Extensions.DependencyInjection.Specification.Fakes.FakeDisposableCallbackInnerService\"",
+                "    },",
+                "    {",
+                "      \"serviceType\": \"Microsoft.Extensions.DependencyInjection.Specification.Fakes.IFakeMultipleService\",",
+                "      \"lifetime\": \"Transient\",",
+                "      \"implementationType\": \"Microsoft.Extensions.DependencyInjection.Specification.Fakes.FakeDisposableCallbackInnerService\"",
+                "    },",
+                "    {",
+                "      \"serviceType\": \"Microsoft.Extensions.DependencyInjection.Specification.Fakes.IFakeService\",",
+                "      \"lifetime\": \"Singleton\",",
+                "      \"implementationType\": \"Microsoft.Extensions.DependencyInjection.Specification.Fakes.FakeDisposableCallbackInnerService\"",
+                "    }",
+                "  ]",
+                "}"),
+                JObject.Parse(GetProperty<string>(serviceProviderDescriptorsEvent, "descriptors")).ToString());
+
+            GetProperty<int>(serviceProviderDescriptorsEvent, "serviceProviderHashCode"); // assert hashcode exists as an int
+            Assert.Equal(0, GetProperty<int>(serviceProviderDescriptorsEvent, "chunkIndex"));
+            Assert.Equal(1, GetProperty<int>(serviceProviderDescriptorsEvent, "chunkCount"));
+            Assert.Equal(8, serviceProviderDescriptorsEvent.EventId);
+        }
+
+        /// <summary>
+        /// Verifies that when an EventListener is enabled after the ServiceProvider has been built,
+        /// the ServiceProviderBuilt events fire. This way users can get ServiceProvider info when
+        /// attaching while the app is running.
+        /// </summary>
+        [Fact]
+        public void EmitsServiceProviderBuiltOnAttach()
+        {
+            _listener.DisableEvents(DependencyInjectionEventSource.Log);
+
+            ServiceCollection serviceCollection = new();
+            serviceCollection.AddSingleton(new FakeDisposeCallback());
+
+            using ServiceProvider provider = serviceCollection.BuildServiceProvider();
+
+            Assert.Empty(_listener.EventData);
+
+            _listener.EnableEvents(DependencyInjectionEventSource.Log, EventLevel.Verbose);
+
+            EventWrittenEventArgs serviceProviderBuiltEvent = _listener.EventData.Single(e => e.EventName == "ServiceProviderBuilt");
+            Assert.Equal(1, GetProperty<int>(serviceProviderBuiltEvent, "singletonServices"));
+
+            EventWrittenEventArgs serviceProviderDescriptorsEvent = _listener.EventData.Single(e => e.EventName == "ServiceProviderDescriptors");
+            Assert.NotNull(JObject.Parse(GetProperty<string>(serviceProviderDescriptorsEvent, "descriptors")));
+        }
+
         private T GetProperty<T>(EventWrittenEventArgs data, string propName)
             => (T)data.Payload[data.PayloadNames.IndexOf(propName)];