diff --git a/Changelog.md b/Changelog.md index 4575e62..324c5fc 100644 --- a/Changelog.md +++ b/Changelog.md @@ -10,6 +10,8 @@ ### Bug Fixes +- Preserve native constructor selection for decorated original type registrations, including ignored preferred-constructor attributes, parameter-type-set ambiguity, contextual keys, generic constraints and native-provider metadata probing, while retaining factory/map activation policies, layer lifetimes and disposal ownership [#111](https://github.com/PrimordialCode/Mammoth.Extensions.DependencyInjection/issues/111). + - Preserve registered factory null results in mapped and contextual keyed constructor dependencies, including named key maps and decoration. Apply optional defaults only to missing registrations while retaining missing-required failures and original factory exceptions [#110](https://github.com/PrimordialCode/Mammoth.Extensions.DependencyInjection/issues/110). - Keep keyed decorator inner binding separate from ServiceKey and FromKeyedServices parameters, preserving explicit, inherited and null-key dependency lookup independently of parameter order, repeated layers, native lifetimes and disposal ownership [#109](https://github.com/PrimordialCode/Mammoth.Extensions.DependencyInjection/issues/109). diff --git a/docs/keyed-built-in-registration-probing.md b/docs/keyed-built-in-registration-probing.md index d776540..964af11 100644 --- a/docs/keyed-built-in-registration-probing.md +++ b/docs/keyed-built-in-registration-probing.md @@ -63,6 +63,16 @@ Before reading the descriptor array, the code verifies that the field exists and This explanation concerns the new private-DI registration fallback. Constructor/parameter reflection used by the existing activator is a separate mechanism. +## Original type activation in decorators + +[Issue #111](https://github.com/PrimordialCode/Mammoth.Extensions.DependencyInjection/issues/111) reuses the native-compatible activator for original implementation-type descriptors hidden by decoration. It ignores `ActivatorUtilitiesConstructorAttribute`, accepts satisfiable constructor parameter permutations/subsets, and rejects unrelated parameter-type sets before activating dependencies. New decorators and nonempty DependsOn factories keep their existing construction policies. + +This activator uses the same keyed built-in availability helper described above. It also validates open-generic constraints for every examined constructor candidate, including candidates that would not ultimately be selected. Native DI performs that validation while constructing argument call sites; its public availability probes report a registration without testing the implementation's generic constraints. Resolving the dependency to check constraints would activate factories too early. + +Mammoth providers supply their existing registration snapshot. For a recognized native probe, the guarded `ReadNativeDescriptors` helper reads the same copied descriptor array through the existing cached field lookup, then builds an internal snapshot cached separately in a `ConditionalWeakTable`. This additional snapshot retains descriptor identities, lifetimes and implementation metadata needed for constraint validation. The keyed-built-in identity cache still stores only the four built-in types. Both caches are provider-specific and weakly keyed; neither reads the subsequently mutable caller collection or activates a service. Exact closed registrations, including AnyKey, take precedence over open-generic bindings. + +Custom probes continue to supply their public availability contract. Missing or incompatible metadata on the recognized native probe fails clearly with the existing recommendation to use Mammoth's provider factory. This reuse adds no new private member lookup; it broadens the descriptor fallback's use to native-compatible generic constraint validation. + ## Verification of the compatibility choice The initial implementation in [PR #115](https://github.com/PrimordialCode/Mammoth.Extensions.DependencyInjection/pull/115), commit `3c6942bfea31dfb1dcd2e3446cb05808a3835810`, was verified as follows. Revalidate these contracts when changing the fallback or upgrading the DI dependency. diff --git a/src/Mammoth.Extensions.DependencyInjection.Tests/ServiceCollectionExtensions.Decorators.NativeConstructors.Tests.cs b/src/Mammoth.Extensions.DependencyInjection.Tests/ServiceCollectionExtensions.Decorators.NativeConstructors.Tests.cs new file mode 100644 index 0000000..9c7b127 --- /dev/null +++ b/src/Mammoth.Extensions.DependencyInjection.Tests/ServiceCollectionExtensions.Decorators.NativeConstructors.Tests.cs @@ -0,0 +1,322 @@ +using Mammoth.Extensions.DependencyInjection.Configuration; +using Microsoft.Extensions.DependencyInjection; +using static Mammoth.Extensions.DependencyInjection.Tests.DiagnosticConstructorParityTests; + +namespace Mammoth.Extensions.DependencyInjection.Tests; + +[TestClass] +public class DecoratedNativeConstructorRegressionTests +{ + public static IEnumerable ConstructorCases() + { + foreach (var type in new[] { typeof(Preferred), typeof(MultiplePreferred), typeof(UnavailablePreferred), + typeof(Permuted), typeof(Subset), typeof(Repeated), typeof(Optional), typeof(RejectLonger), + typeof(DifferentParameters), typeof(EqualLengthDifferentParameters) }) + foreach (var kind in new[] { "native", "snapshot", "diagnostics" }) + foreach (var lifetime in new[] { ServiceLifetime.Transient, ServiceLifetime.Scoped, ServiceLifetime.Singleton }) + foreach (var keyed in new[] { false, true }) + yield return [type, kind, lifetime, keyed]; + } + + [TestMethod] + [DynamicData(nameof(ConstructorCases))] + public void OriginalTypeKeepsNativeConstructorRulesAcrossLayersAndLifetimes( + Type type, string kind, ServiceLifetime lifetime, bool keyed) + { + var nativeCounts = new Counts(); + var counts = new Counts(); + var nativeServices = Services(nativeCounts); + var services = Services(counts); + Add(nativeServices, type, lifetime, keyed); + Add(services, type, lifetime, keyed); + services.Decorate(); + services.Decorate(); + using var native = nativeServices.BuildServiceProvider(); + using var provider = Build(services, kind); + Assert.AreEqual(0, counts.Dependencies); + using var nativeScope = native.CreateScope(); + using var scope = provider.CreateScope(); + if (type == typeof(DifferentParameters) || type == typeof(EqualLengthDifferentParameters)) + { + Assert.ThrowsExactly(() => Resolve(nativeScope.ServiceProvider, keyed)); + var error = Assert.ThrowsExactly(() => Resolve(scope.ServiceProvider, keyed)); + StringAssert.Contains(error.Message, "ambiguous"); + Assert.AreEqual(0, nativeCounts.Dependencies); + Assert.AreEqual(0, counts.Dependencies); + Assert.AreEqual(0, counts.Constructors); + return; + } + var nativeResult = Resolve(nativeScope.ServiceProvider, keyed); + var first = Resolve(scope.ServiceProvider, keyed); + var firstInner = Original(first); + Assert.AreEqual(type, firstInner.GetType()); + Assert.AreEqual(nativeResult.Selected, first.Selected); + var again = Resolve(scope.ServiceProvider, keyed); + using var other = provider.CreateScope(); + var acrossScopes = Resolve(other.ServiceProvider, keyed); + Assert.AreEqual(nativeResult.Selected, again.Selected); + Assert.AreEqual(nativeResult.Selected, acrossScopes.Selected); + if (lifetime == ServiceLifetime.Transient) + { + Assert.AreNotSame(first, again); + Assert.AreNotSame(firstInner, Original(again)); + } + else + { + Assert.AreSame(first, again); + Assert.AreSame(firstInner, Original(again)); + } + if (lifetime == ServiceLifetime.Singleton) Assert.AreSame(first, acrossScopes); + else Assert.AreNotSame(first, acrossScopes); + Assert.AreEqual(0, counts.Rejected, "A rejected constructor must not activate its dependencies."); + } + + [TestMethod] + [DataRow("native", false)] + [DataRow("native", true)] + [DataRow("snapshot", false)] + [DataRow("snapshot", true)] + [DataRow("diagnostics", false)] + [DataRow("diagnostics", true)] + public void ContextualOriginalKeepsRequestedKeyAndAttributeBindings(string kind, bool wildcard) + { + IServiceCollection services = new ServiceCollection(); + var ordinary = new A(); + var inherited = new A(); + var explicitKey = new A(); + services.AddSingleton(ordinary); + services.AddKeyedSingleton("blue", inherited); + services.AddKeyedSingleton("fixed", explicitKey); + services.AddKeyedTransient(wildcard ? KeyedService.AnyKey : "blue"); + using var native = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = kind == "diagnostics" }); + var key = new string("blue".ToCharArray()); + var nativeResult = (Contextual)native.GetRequiredKeyedService(key); + services.Decorate(); + services.Decorate(); + using var provider = Build(services, kind); + var result = (Contextual)Original(provider.GetRequiredKeyedService(key)); + Assert.AreEqual(nativeResult.Selected, result.Selected); + // Build validation can cache the registration's equal key before the first + // request. Compare key identity under the same native validation setting. + Assert.AreSame(nativeResult.Key, result.Key); + Assert.AreSame(ordinary, result.Ordinary); + Assert.AreSame(inherited, result.Inherited); + Assert.AreSame(explicitKey, result.Explicit); + } + + public static IEnumerable MetadataCases() + { + foreach (var kind in new[] { "native", "snapshot", "diagnostics" }) + foreach (var keyed in new[] { false, true }) + foreach (var scenario in new[] { "builtin-missing", "builtin-exact", "builtin-wildcard", + "generic-invalid", "generic-closed", "generic-wildcard-closed", "generic-inherited" }) + if (scenario != "generic-inherited" || keyed) yield return [kind, keyed, scenario]; + } + + [TestMethod] + [DynamicData(nameof(MetadataCases))] + public void NativeProvidersPreserveBuiltInAvailabilityAndGenericConstraintChecks( + string kind, bool keyed, string scenario) + { + var nativeCounts = new Counts(); + var counts = new Counts(); + var explicitProvider = new EmptyProvider(); + var nativeServices = Configure(nativeCounts); + var services = Configure(counts); + using var native = nativeServices.BuildServiceProvider(); + services.Decorate(); + using var provider = Build(services, kind); + // Mutating the caller collection must not replace a built provider's metadata. + services.AddKeyedTransient, Open>("bad"); + services.AddKeyedSingleton("blue", explicitProvider); + if (scenario == "generic-invalid") + { + Assert.ThrowsExactly(() => Resolve(native, keyed)); + Assert.ThrowsExactly(() => Resolve(provider, keyed)); + Assert.AreEqual(0, nativeCounts.Dependencies); + Assert.AreEqual(0, counts.Dependencies); + Assert.AreEqual(0, counts.Constructors); + } + else + { + var nativeResult = Resolve(native, keyed); + var result = Original(Resolve(provider, keyed), layers: 1); + Assert.AreEqual(nativeResult.Selected, result.Selected); + if (result is KeyedBuiltIn builtIn) + Assert.AreSame(scenario == "builtin-missing" ? null : explicitProvider, builtIn.Provider); + } + + IServiceCollection Configure(Counts tracker) + { + var collection = Services(tracker); + Type target; + if (scenario.StartsWith("builtin", StringComparison.Ordinal)) + { + target = typeof(KeyedBuiltIn); + if (scenario != "builtin-missing") collection.AddKeyedSingleton( + scenario == "builtin-exact" ? "blue" : KeyedService.AnyKey, explicitProvider); + } + else if (scenario == "generic-inherited") + { + target = typeof(InheritedGeneric); + collection.AddKeyedTransient(typeof(IOpen<>), KeyedService.AnyKey, typeof(Open<>)); + } + else + { + target = typeof(ConstrainedCandidate); + collection.AddTransient>(_ => { tracker.Dependencies++; return new Open(); }); + collection.AddKeyedTransient(typeof(IOpen<>), "bad", typeof(ReferenceOnly<>)); + if (scenario != "generic-invalid") collection.AddKeyedTransient, Open>( + scenario == "generic-closed" ? "bad" : KeyedService.AnyKey); + } + Add(collection, target, ServiceLifetime.Transient, keyed); + return collection; + } + } + + public static IEnumerable OwnershipCases() + { + foreach (var kind in new[] { "native", "snapshot", "diagnostics" }) + foreach (var keyed in new[] { false, true }) + foreach (var registration in new[] { "type", "factory", "instance" }) + foreach (var lifetime in new[] { ServiceLifetime.Transient, ServiceLifetime.Scoped, ServiceLifetime.Singleton }) + if (registration != "instance" || lifetime == ServiceLifetime.Singleton) + yield return [kind, keyed, registration, lifetime]; + } + + [TestMethod] + [DynamicData(nameof(OwnershipCases))] + public void RepeatedLayersPreserveFactoryInstanceLifetimesAndDisposalOwnership( + string kind, bool keyed, string registration, ServiceLifetime lifetime) + { + var tracker = new Tracker(); + var services = Services(new Counts()); + services.AddSingleton(tracker); + var borrowed = new DisposablePreferred(new A()); + var factoryCalls = 0; + services.Add(registration switch + { + "instance" => keyed ? ServiceDescriptor.KeyedSingleton("blue", borrowed) : ServiceDescriptor.Singleton(borrowed), + "factory" => keyed ? ServiceDescriptor.DescribeKeyed(typeof(IChoice), "blue", (_, _) => Create(), lifetime) + : ServiceDescriptor.Describe(typeof(IChoice), _ => Create(), lifetime), + _ => keyed ? ServiceDescriptor.DescribeKeyed(typeof(IChoice), "blue", typeof(DisposablePreferred), lifetime) + : ServiceDescriptor.Describe(typeof(IChoice), typeof(DisposablePreferred), lifetime) + }); + services.Decorate(); + services.Decorate(); + using var provider = Build(services, kind); + var originals = new HashSet(); + IChoice? first = null; + for (var scopeIndex = 0; scopeIndex < 2; scopeIndex++) + { + using (var scope = provider.CreateScope()) + for (var repeat = 0; repeat < 2; repeat++) + { + var result = Resolve(scope.ServiceProvider, keyed); + var inner = (DisposablePreferred)Original(result); + originals.Add(inner); + Assert.AreEqual(registration == "type" ? "AB" : "A", result.Selected); + if (registration == "instance") Assert.AreSame(borrowed, inner); + if (first == null) first = result; + else if (lifetime == ServiceLifetime.Singleton || (lifetime == ServiceLifetime.Scoped && scopeIndex == 0)) + Assert.AreSame(first, result); + else Assert.AreNotSame(first, result); + } + foreach (var inner in originals) + Assert.AreEqual(registration == "instance" || lifetime == ServiceLifetime.Singleton ? 0 : 1, inner.Disposals); + } + Assert.IsNotEmpty(originals); + Assert.IsNotEmpty(tracker.Created); + var expectedInstances = lifetime == ServiceLifetime.Transient ? 4 : lifetime == ServiceLifetime.Scoped ? 2 : 1; + Assert.HasCount(expectedInstances, originals); + Assert.HasCount(expectedInstances * 2, tracker.Created); + Assert.AreEqual(registration == "factory" ? expectedInstances : 0, factoryCalls); + provider.Dispose(); + foreach (var wrapper in tracker.Created) Assert.AreEqual(1, wrapper.Disposals); + foreach (var inner in originals) Assert.AreEqual(registration == "instance" ? 0 : 1, inner.Disposals); + + DisposablePreferred Create() { factoryCalls++; return new DisposablePreferred(new A()); } + } + + [TestMethod] + [DataRow("native", false)] + [DataRow("native", true)] + [DataRow("snapshot", false)] + [DataRow("snapshot", true)] + [DataRow("diagnostics", false)] + [DataRow("diagnostics", true)] + public void NonemptyMapsAndNewDecoratorsKeepTheirPreferredConstructorPolicy(string kind, bool keyed) + { + var services = Services(new Counts()); + Dependency[] map = [Dependency.OnValue("a", new A())]; + if (keyed) services.AddKeyedTransient("blue", map); + else services.AddTransient(map); + services.Decorate(); + using var provider = Build(services, kind); + var wrapper = (PolicyForwarder)Resolve(provider, keyed); + Assert.AreEqual("short", wrapper.Policy); + Assert.AreEqual("A", wrapper.Inner.Selected); + } + + [TestMethod] + [DataRow("native", false)] + [DataRow("native", true)] + [DataRow("snapshot", false)] + [DataRow("snapshot", true)] + [DataRow("diagnostics", false)] + [DataRow("diagnostics", true)] + public void OriginalConstructorExceptionsKeepTheirType(string kind, bool keyed) + { + var services = Services(new Counts()); + Add(services, typeof(Throwing), ServiceLifetime.Transient, keyed); + services.Decorate(); + using var provider = Build(services, kind); + Assert.ThrowsExactly(() => Resolve(provider, keyed)); + } + + private static IServiceCollection Services(Counts counts) + { + IServiceCollection services = new ServiceCollection(); + services.AddSingleton(counts); + services.AddTransient(_ => { counts.Dependencies++; return new A(); }); + services.AddTransient(_ => { counts.Dependencies++; return new B(); }); + services.AddTransient(_ => { counts.Dependencies++; return new C(); }); + services.AddTransient(_ => { counts.Rejected++; return new Rejected(); }); + return services; + } + private static void Add(IServiceCollection services, Type type, ServiceLifetime lifetime, bool keyed) + => services.Add(keyed ? ServiceDescriptor.DescribeKeyed(typeof(IChoice), "blue", type, lifetime) + : ServiceDescriptor.Describe(typeof(IChoice), type, lifetime)); + private static IChoice Resolve(IServiceProvider provider, bool keyed) => keyed + ? provider.GetRequiredKeyedService("blue") : provider.GetRequiredService(); + private static IChoice Original(IChoice choice, int layers = 2) + { + for (var index = 0; index < layers; index++) choice = ((Forwarder)choice).Inner; + return choice; + } + private static ServiceProvider Build(IServiceCollection services, string kind) => kind switch + { + "snapshot" => ServiceProviderFactory.CreateServiceProvider(services), + "diagnostics" => ServiceProviderFactory.CreateServiceProvider(services, + new ExtendedServiceProviderOptions { ValidateOnBuild = true, DetectIncorrectUsageOfTransientDisposables = true }), + _ => services.BuildServiceProvider() + }; + public class Forwarder(IChoice inner) : IChoice + { + public IChoice Inner { get; } = inner; + public string Selected => Inner.Selected; + } + public sealed class Tracker { public List Created { get; } = []; } + public sealed class OwnedForwarder : Forwarder, IDisposable + { + public int Disposals { get; private set; } + public OwnedForwarder(IChoice inner, Tracker tracker) : base(inner) => tracker.Created.Add(this); + public void Dispose() => Disposals++; + } + public sealed class PolicyForwarder : Forwarder + { + public string Policy { get; } + [ActivatorUtilitiesConstructor] public PolicyForwarder(IChoice inner) : base(inner) => Policy = "short"; + public PolicyForwarder(IChoice inner, A a) : base(inner) => Policy = "long"; + } +} diff --git a/src/Mammoth.Extensions.DependencyInjection/ConstructorActivator.cs b/src/Mammoth.Extensions.DependencyInjection/ConstructorActivator.cs index 37f2b6f..98966df 100644 --- a/src/Mammoth.Extensions.DependencyInjection/ConstructorActivator.cs +++ b/src/Mammoth.Extensions.DependencyInjection/ConstructorActivator.cs @@ -25,7 +25,7 @@ private static bool IsBuiltIn(Type type) => type == typeof(IServiceProvider) || type == typeof(IServiceScopeFactory) || type == typeof(IServiceProviderIsService) || type == typeof(IServiceProviderIsKeyedService); - private static bool IsKeyedBuiltInRegistered(IServiceProvider provider, IServiceProviderIsKeyedService probe, Type type, object key) + internal static bool IsKeyedBuiltInRegistered(IServiceProvider provider, IServiceProviderIsKeyedService probe, Type type, object key) { // Mammoth's immutable snapshot is the preferred source, including instrumented // providers. Custom probes retain their own availability contract. @@ -36,9 +36,7 @@ private static bool IsKeyedBuiltInRegistered(IServiceProvider provider, IService { // Guard native internals: changed or unavailable metadata must fail clearly // rather than guess availability or invoke a dependency factory. - if (NativeDescriptors?.FieldType != typeof(ServiceDescriptor[]) - || NativeDescriptors.GetValue(nativeProbe) is not ServiceDescriptor[] descriptors) - throw new NotSupportedException("Native keyed built-in constructor selection requires registration metadata. Use Mammoth's ServiceProviderFactory with this provider version."); + var descriptors = ReadNativeDescriptors(nativeProbe); var identities = new HashSet(); foreach (var descriptor in descriptors) if (descriptor.IsKeyedService && descriptor.ServiceKey != null && IsBuiltIn(descriptor.ServiceType)) @@ -49,6 +47,28 @@ private static bool IsKeyedBuiltInRegistered(IServiceProvider provider, IService || registrations.Contains(new ServiceIdentifier(KeyedService.AnyKey, type)); } + private static readonly ConditionalWeakTable NativeSnapshots = new(); + + internal static ServiceProviderRegistrationSnapshot? GetRegistrationSnapshot(IServiceProvider provider, IServiceProviderIsKeyedService probe) + { + if (provider.GetService() is { } snapshot) return snapshot; + if (probe.GetType() != NativeProbeType) return null; + // Public probes cannot validate open-generic constraints on unselected candidates. + // Reuse the guarded copied-descriptor fallback for original type activation, so + // native providers need no Mammoth setup and no factory runs during selection. + // See docs/keyed-built-in-registration-probing.md for both metadata use cases. + return NativeSnapshots.GetValue(probe, static nativeProbe => new ServiceProviderRegistrationSnapshot( + ReadNativeDescriptors(nativeProbe), [], [], [])); + } + + private static ServiceDescriptor[] ReadNativeDescriptors(IServiceProviderIsKeyedService nativeProbe) + { + if (NativeDescriptors?.FieldType != typeof(ServiceDescriptor[]) + || NativeDescriptors.GetValue(nativeProbe) is not ServiceDescriptor[] descriptors) + throw new NotSupportedException("Native constructor selection requires registration metadata. Use Mammoth's ServiceProviderFactory with this provider version."); + return descriptors; + } + internal static object CreateKeyed(IServiceProvider provider, Type target, object? serviceKey, object? inner = null) { // Keep ActivatorUtilities behavior for constructors without contextual parameters. diff --git a/src/Mammoth.Extensions.DependencyInjection/NativeConstructorActivator.cs b/src/Mammoth.Extensions.DependencyInjection/NativeConstructorActivator.cs index 965906d..8253fa5 100644 --- a/src/Mammoth.Extensions.DependencyInjection/NativeConstructorActivator.cs +++ b/src/Mammoth.Extensions.DependencyInjection/NativeConstructorActivator.cs @@ -4,7 +4,7 @@ namespace Mammoth.Extensions.DependencyInjection; -// Diagnostics replaces ordinary implementation-type descriptors with tracking factories. +// Diagnostics and decoration replace implementation-type descriptors with factories. // Keep native DI's constructor rules separate from ActivatorUtilities / DependsOn rules. internal static class NativeConstructorActivator { @@ -107,15 +107,14 @@ internal static object CreateInstance(IServiceProvider provider, Type implementa bool IsRegistered(Type type, object? key) { if (type.IsConstructedGenericType) - provider.GetRequiredService().ValidateGenericConstraints(type, key); + ConstructorActivator.GetRegistrationSnapshot(provider, keyedProbe)?.ValidateGenericConstraints(type, key); if (key == null) return probe.IsService(type); // Native's public keyed probe reports built-ins for every key, although // their implicit call sites are unkeyed. Explicit keyed registrations work. if (type == typeof(IServiceProvider) || type == typeof(IServiceScopeFactory) || type == typeof(IServiceProviderIsService) || type == typeof(IServiceProviderIsKeyedService)) { - var snapshot = provider.GetRequiredService(); - return snapshot.GetLifetime(type, key) != null || snapshot.GetLifetime(type, KeyedService.AnyKey) != null; + return ConstructorActivator.IsKeyedBuiltInRegistered(provider, keyedProbe, type, key); } // The DI 10 public probe does not check AnyKey open-generic fallback. return keyedProbe.IsKeyedService(type, key) diff --git a/src/Mammoth.Extensions.DependencyInjection/ServiceCollectionExtensions.Decorators.cs b/src/Mammoth.Extensions.DependencyInjection/ServiceCollectionExtensions.Decorators.cs index c1f29d7..fcd18c5 100644 --- a/src/Mammoth.Extensions.DependencyInjection/ServiceCollectionExtensions.Decorators.cs +++ b/src/Mammoth.Extensions.DependencyInjection/ServiceCollectionExtensions.Decorators.cs @@ -80,8 +80,10 @@ private static object CreateRegisteredInstance(IServiceProvider provider, Servic if (!keyed && descriptor.ImplementationFactory != null) return descriptor.ImplementationFactory(provider); var implementation = keyed ? descriptor.KeyedImplementationType! : descriptor.ImplementationType!; - return keyed ? ConstructorActivator.CreateKeyed(provider, implementation, requestedKey) - : ActivatorUtilities.CreateInstance(provider, implementation); + // Reactivate an original type descriptor with native DI constructor rules. + // Factory descriptors above retain their own activation policy (including maps + // and preceding decorators). An unkeyed original must not see the private slot key. + return NativeConstructorActivator.CreateInstance(provider, implementation, keyed ? requestedKey : null); } internal static bool IsDecorationSlot(Type type) =>