diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/DependencyInjectionEventSource.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/DependencyInjectionEventSource.cs index 32f7596b393105..338325edb830f6 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/DependencyInjectionEventSource.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/DependencyInjectionEventSource.cs @@ -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 @@ internal sealed class DependencyInjectionEventSource : EventSource { 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> _providers = new(); + private DependencyInjectionEventSource() : base(EventSourceSettings.EtwSelfDescribingEventFormat) { } @@ -32,27 +41,27 @@ private DependencyInjectionEventSource() : base(EventSourceSettings.EtwSelfDescr [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 @@ public void ScopeDisposed(int serviceProviderHashCode, int scopedServicesResolve } [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(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 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 reference in _providers) + { + if (reference.TryGetTarget(out ServiceProvider provider)) + { + WriteServiceProviderBuilt(provider); + } + } + } } } } @@ -117,13 +280,13 @@ internal static class DependencyInjectionEventSourceExtensions // 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()); } } diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs index 06bcdf632879ca..02a754b3214bee 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs @@ -29,6 +29,8 @@ public CallSiteFactory(IEnumerable descriptors) Populate(); } + internal ServiceDescriptor[] Descriptors => _descriptors; + private void Populate() { foreach (ServiceDescriptor descriptor in _descriptors) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/DynamicServiceProviderEngine.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/DynamicServiceProviderEngine.cs index 0237e8ad096b74..09abe625a658c6 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/DynamicServiceProviderEngine.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/DynamicServiceProviderEngine.cs @@ -37,7 +37,7 @@ public override Func RealizeService(ServiceC } 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}"); } diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/Expressions/ExpressionResolverBuilder.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/Expressions/ExpressionResolverBuilder.cs index 4fbed795a9a812..c95856af4f4890 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/Expressions/ExpressionResolverBuilder.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/Expressions/ExpressionResolverBuilder.cs @@ -68,7 +68,7 @@ public Func Build(ServiceCallSite callSite) public Func BuildNoCache(ServiceCallSite callSite) { Expression> expression = BuildExpression(callSite); - DependencyInjectionEventSource.Log.ExpressionTreeGenerated(callSite.ServiceType, expression); + DependencyInjectionEventSource.Log.ExpressionTreeGenerated(_rootScope.RootProvider, callSite.ServiceType, expression); return expression.Compile(); } diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ILEmit/ILEmitResolverBuilder.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ILEmit/ILEmitResolverBuilder.cs index 6cc6ba36b96978..bff244ac12d16d 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ILEmit/ILEmitResolverBuilder.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ILEmit/ILEmitResolverBuilder.cs @@ -112,7 +112,7 @@ private GeneratedMethod BuildTypeNoCache(ServiceCallSite callSite) 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() { diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs index a05e82f8a44986..9c1c5c6480ee64 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs @@ -75,6 +75,7 @@ internal ServiceProvider(IEnumerable serviceDescriptors, Serv } } + DependencyInjectionEventSource.Log.ServiceProviderBuilt(this); } /// @@ -87,17 +88,23 @@ internal ServiceProvider(IEnumerable serviceDescriptors, Serv /// public void Dispose() { - _disposed = true; + DisposeCore(); Root.Dispose(); } /// 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 @@ internal object GetService(Type serviceType, ServiceProviderEngineScope serviceP Func 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 @@ private Func CreateServiceAccessor(Type serv 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 diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/DependencyInjectionEventSourceTests.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/DependencyInjectionEventSourceTests.cs index 54a282d66dd9b6..11d8ae91570ecc 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/DependencyInjectionEventSourceTests.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/DependencyInjectionEventSourceTests.cs @@ -6,6 +6,7 @@ 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 @@ public class EventSourceTests : ICollectionFixture } [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>()); + _listener.EnableEvents(DependencyInjectionEventSource.Log, EventLevel.Verbose); } @@ -214,15 +219,114 @@ public void EmitsDynamicMethodBuiltEvent() 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(serviceRealizationFailedEvent, "exceptionMessage")); + Assert.Equal(1234, GetProperty(serviceRealizationFailedEvent, "serviceProviderHashCode")); Assert.Equal(6, serviceRealizationFailedEvent.EventId); } + [Fact] + public void EmitsServiceProviderBuilt() + { + ServiceCollection serviceCollection = new(); + FakeDisposeCallback fakeDisposeCallback = new(); + serviceCollection.AddSingleton(fakeDisposeCallback); + serviceCollection.AddTransient(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(provider => new FakeDisposableCallbackInnerService(fakeDisposeCallback)); + serviceCollection.AddScoped(); + serviceCollection.AddTransient(); + serviceCollection.AddSingleton(); + + using ServiceProvider provider = serviceCollection.BuildServiceProvider(); + + EventWrittenEventArgs serviceProviderBuiltEvent = _listener.EventData.Single(e => e.EventName == "ServiceProviderBuilt"); + GetProperty(serviceProviderBuiltEvent, "serviceProviderHashCode"); // assert hashcode exists as an int + Assert.Equal(4, GetProperty(serviceProviderBuiltEvent, "singletonServices")); + Assert.Equal(1, GetProperty(serviceProviderBuiltEvent, "scopedServices")); + Assert.Equal(2, GetProperty(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 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(serviceProviderDescriptorsEvent, "descriptors")).ToString()); + + GetProperty(serviceProviderDescriptorsEvent, "serviceProviderHashCode"); // assert hashcode exists as an int + Assert.Equal(0, GetProperty(serviceProviderDescriptorsEvent, "chunkIndex")); + Assert.Equal(1, GetProperty(serviceProviderDescriptorsEvent, "chunkCount")); + Assert.Equal(8, serviceProviderDescriptorsEvent.EventId); + } + + /// + /// 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. + /// + [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(serviceProviderBuiltEvent, "singletonServices")); + + EventWrittenEventArgs serviceProviderDescriptorsEvent = _listener.EventData.Single(e => e.EventName == "ServiceProviderDescriptors"); + Assert.NotNull(JObject.Parse(GetProperty(serviceProviderDescriptorsEvent, "descriptors"))); + } + private T GetProperty(EventWrittenEventArgs data, string propName) => (T)data.Payload[data.PayloadNames.IndexOf(propName)];