diff --git a/Changelog.md b/Changelog.md index 5f367a0..d8798aa 100644 --- a/Changelog.md +++ b/Changelog.md @@ -10,6 +10,8 @@ ### Bug Fixes +- Restore typed resolution in `GetAllServices()` for value-type services such as `int`, preserving explicit enumerable registrations, snapshot-backed key merging, AnyKey exclusion and private decorator filtering [#85](https://github.com/PrimordialCode/Mammoth.Extensions.DependencyInjection/issues/85). + - Preserve exact public-service diagnostic exclusions through every private decorator layer, including repeated keyed decoration, while retaining unrelated dependency checks and container disposal ownership [#91](https://github.com/PrimordialCode/Mammoth.Extensions.DependencyInjection/issues/91). - Normalize optional nullable-enum defaults in mapped activation and contextual keyed decorators, preserving null defaults, named overrides and registered-service precedence [#82](https://github.com/PrimordialCode/Mammoth.Extensions.DependencyInjection/issues/82). diff --git a/src/Mammoth.Extensions.DependencyInjection.Tests/ServiceProviderExtensions.ValueTypes.Tests.cs b/src/Mammoth.Extensions.DependencyInjection.Tests/ServiceProviderExtensions.ValueTypes.Tests.cs new file mode 100644 index 0000000..4a385fb --- /dev/null +++ b/src/Mammoth.Extensions.DependencyInjection.Tests/ServiceProviderExtensions.ValueTypes.Tests.cs @@ -0,0 +1,177 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Mammoth.Extensions.DependencyInjection.Tests; + +// refs::#85 +[TestClass] +public class ValueTypeEnumerationRegressionTests +{ + [TestMethod] + [DataRow(false, false, false)] + [DataRow(false, true, false)] + [DataRow(true, false, false)] + [DataRow(true, true, false)] + [DataRow(false, false, true)] + [DataRow(false, true, true)] + [DataRow(true, false, true)] + [DataRow(true, true, true)] + public void IntInstancesPreserveNativeGroups(bool unkeyed, bool keyed, bool diagnostics) + { + AssertInstanceGroups(42, 24, unkeyed, keyed, diagnostics); + } + + [TestMethod] + [DataRow(false, false, false)] + [DataRow(false, true, false)] + [DataRow(true, false, false)] + [DataRow(true, true, false)] + [DataRow(false, false, true)] + [DataRow(false, true, true)] + [DataRow(true, false, true)] + [DataRow(true, true, true)] + public void StructInstancesPreserveNativeGroups(bool unkeyed, bool keyed, bool diagnostics) + { + AssertInstanceGroups(new ValueService(42), new ValueService(24), unkeyed, keyed, diagnostics); + } + + [TestMethod] + [DataRow(false)] + [DataRow(true)] + public void ReferenceInstancesKeepOrderingAndMultiplicity(bool diagnostics) + { + AssertInstanceGroups("unkeyed", "keyed", unkeyed: true, keyed: true, diagnostics: diagnostics); + } + + [TestMethod] + [DataRow(false, false, false)] + [DataRow(false, true, false)] + [DataRow(true, false, false)] + [DataRow(true, true, false)] + [DataRow(false, false, true)] + [DataRow(false, true, true)] + [DataRow(true, false, true)] + [DataRow(true, true, true)] + public void ExplicitIntEnumerableKeepsPrecedenceAndDuplicates(bool factory, bool empty, bool diagnostics) + { + var services = new ServiceCollection(); + services.AddSingleton(typeof(int), 99); + services.AddSingleton>(new[] { -1 }); + var supplied = empty ? Array.Empty() : new[] { 42, 42, 43 }; + if (factory) + services.AddSingleton>(_ => supplied); + else + services.AddSingleton>(supplied); + services.AddKeyedSingleton(typeof(int), "blue", 24); + services.AddKeyedSingleton(typeof(int), "blue", 25); + using var provider = Build(services, diagnostics); + + CollectionAssert.AreEqual(supplied, provider.GetServices().ToArray()); + CollectionAssert.AreEqual(new[] { 24, 25 }, provider.GetKeyedServices("blue").ToArray()); + var expected = supplied.Concat(new[] { 24, 25 }).ToArray(); + CollectionAssert.AreEqual(expected, provider.GetAllServices().ToArray()); + } + + [TestMethod] + [DataRow(false)] + [DataRow(true)] + public void ExplicitKeyedIntEnumerableIsResolvedForDiscoveredKey(bool diagnostics) + { + var services = new ServiceCollection(); + services.AddKeyedSingleton(typeof(int), "blue", 99); + services.AddKeyedSingleton>("blue", new[] { 24, 24, 25 }); + using var provider = Build(services, diagnostics); + + CollectionAssert.AreEqual(new[] { 24, 24, 25 }, provider.GetKeyedServices("blue").ToArray()); + CollectionAssert.AreEqual(new[] { 24, 24, 25 }, provider.GetAllServices().ToArray()); + } + + [TestMethod] + [DataRow(false)] + [DataRow(true)] + public void SnapshotKeysSurvivePublicMutationAndExcludeAnyKey(bool diagnostics) + { + var wildcardCalls = 0; + var services = new ServiceCollection(); + services.AddSingleton(typeof(int), 42); + services.AddKeyedSingleton(typeof(int), "blue", 24); + services.AddKeyedSingleton(typeof(int), "blue", 24); + services.AddKeyedSingleton(typeof(int), KeyedService.AnyKey, (_, _) => + { + wildcardCalls++; + return 99; + }); + using var provider = Build(services, diagnostics); + provider.GetRequiredService().Clear(); + provider.GetRequiredService().Clear(); + var keys = provider.GetRequiredService>(); + keys.Clear(); + keys.Add("invented"); + keys.Add(KeyedService.AnyKey); + + for (var i = 0; i < 3; i++) + CollectionAssert.AreEqual(new[] { 42, 24, 24 }, provider.GetAllServices().ToArray()); + Assert.AreEqual(0, wildcardCalls); + } + + [TestMethod] + [DataRow(false)] + [DataRow(true)] + public void NativeProviderKeepsLegacyExplicitKeyMetadata(bool keyed) + { + var services = new ServiceCollection(); + services.AddSingleton(typeof(int), 42); + if (keyed) + { + services.AddKeyedSingleton(typeof(int), "blue", 24); + services.AddSingleton(new ServiceKeys(new object[] { "blue", KeyedService.AnyKey })); + } + using var provider = services.BuildServiceProvider(); + + CollectionAssert.AreEqual(keyed ? new[] { 42, 24 } : new[] { 42 }, provider.GetAllServices().ToArray()); + } + + private static void AssertInstanceGroups(T unkeyedValue, T keyedValue, bool unkeyed, bool keyed, bool diagnostics) + where T : notnull + { + var services = new ServiceCollection(); + if (unkeyed) + { + services.AddSingleton(typeof(T), unkeyedValue); + services.AddSingleton(typeof(T), unkeyedValue); + } + if (keyed) + { + services.AddKeyedSingleton(typeof(T), "blue", keyedValue); + services.AddKeyedSingleton(typeof(T), "blue", keyedValue); + services.AddKeyedSingleton(typeof(T), "green", keyedValue); + } + using var provider = Build(services, diagnostics); + var nativeUnkeyed = provider.GetServices().ToArray(); + var nativeBlue = provider.GetKeyedServices("blue").ToArray(); + var nativeGreen = provider.GetKeyedServices("green").ToArray(); + CollectionAssert.AreEqual(unkeyed ? new[] { unkeyedValue, unkeyedValue } : Array.Empty(), nativeUnkeyed); + CollectionAssert.AreEqual(keyed ? new[] { keyedValue, keyedValue } : Array.Empty(), nativeBlue); + CollectionAssert.AreEqual(keyed ? new[] { keyedValue } : Array.Empty(), nativeGreen); + var expected = nativeUnkeyed.Concat(nativeBlue).Concat(nativeGreen).ToArray(); + + for (var i = 0; i < 3; i++) + { + var actual = provider.GetAllServices().ToArray(); + CollectionAssert.AreEquivalent(expected, actual); + CollectionAssert.AreEqual(nativeUnkeyed, actual.Take(nativeUnkeyed.Length).ToArray()); + } + } + + private static ServiceProvider Build(IServiceCollection services, bool diagnostics) => + ServiceProviderFactory.CreateServiceProvider(services, new ExtendedServiceProviderOptions + { + DetectIncorrectUsageOfTransientDisposables = diagnostics, + ValidateOnBuild = true, + ValidateScopes = true + }); + + private readonly struct ValueService(int value) + { + public int Value { get; } = value; + } +} diff --git a/src/Mammoth.Extensions.DependencyInjection/ServiceProviderExtensions.cs b/src/Mammoth.Extensions.DependencyInjection/ServiceProviderExtensions.cs index 9e54a45..3540f98 100644 --- a/src/Mammoth.Extensions.DependencyInjection/ServiceProviderExtensions.cs +++ b/src/Mammoth.Extensions.DependencyInjection/ServiceProviderExtensions.cs @@ -62,7 +62,17 @@ public static partial class ServiceProviderExtensions /// public static IEnumerable GetAllServices(this IServiceProvider serviceProvider) { - return serviceProvider.GetAllServices(typeof(TServiceType)).Cast(); + var serviceType = typeof(TServiceType); + var snapshot = serviceProvider.GetService(); + var serviceList = new List(); + if (snapshot == null || !ServiceCollectionExtensions.IsDecorationSlot(serviceType)) + serviceList.AddRange(serviceProvider.GetServices()); + var keys = snapshot?.GetKeys(serviceType) ?? + (serviceProvider.GetService(typeof(ServiceKeys)) as IEnumerable ?? []); + foreach (var serviceKey in keys) + if (!ReferenceEquals(serviceKey, KeyedService.AnyKey)) + serviceList.AddRange(serviceProvider.GetKeyedServices(serviceKey)); + return serviceList; } private static InvalidOperationException BuildExceptionBecauseProviderWasNotBuiltUsingTheFactory(Exception? ex = null)