From 86f3061d8af9dc49f488e4804af22644327632e0 Mon Sep 17 00:00:00 2001 From: Nikolay Pianikov Date: Fri, 20 Feb 2026 17:17:54 +0300 Subject: [PATCH 01/15] Add test for complex override --- .../Pure.DI.IntegrationTests/OverrideTests.cs | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) diff --git a/tests/Pure.DI.IntegrationTests/OverrideTests.cs b/tests/Pure.DI.IntegrationTests/OverrideTests.cs index 7605015c2..ba1910247 100644 --- a/tests/Pure.DI.IntegrationTests/OverrideTests.cs +++ b/tests/Pure.DI.IntegrationTests/OverrideTests.cs @@ -2424,6 +2424,224 @@ public static void Main() result.StdOut.ShouldBe(["3", "0", "1", "2"], result); } + [Fact] + public async Task ShouldSupportOverrideInFactoryWithLocalFunctionAndFuncArgs() + { + // Given + + // When + var result = await """ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using Pure.DI; + + namespace Sample + { + interface IStorage {} + + class Storage : IStorage + { + } + + class Command + { + public Command(Func canExecute, Func execute) + { + CanExecute = canExecute; + Execute = execute; + } + + public Func CanExecute { get; } + + public Func Execute { get; } + + public IDispatcher? Dispatcher { get; set; } + } + + interface IDispatcher {} + + class Dispatcher : IDispatcher + { + } + + class NodeName + { + public NodeName(string value) + { + Value = value; + } + + public string Value { get; } + } + + interface ITreeNodeViewModel + { + string Kind { get; } + } + + class DirectoryNodeViewModel : ITreeNodeViewModel + { + public DirectoryNodeViewModel( + Func, Func, Command> commandBuilder, + IStorage storage, + NodeName name, + List children) + { + Kind = $"Dir:{name.Value}:{children.Count}"; + } + + public string Kind { get; } + } + + class FileTreeNodeViewModel : ITreeNodeViewModel + { + public FileTreeNodeViewModel( + Func, Func, Command> commandBuilder, + IStorage storage, + NodeName name) + { + Kind = $"File:{name.Value}"; + } + + public string Kind { get; } + } + + class NodeFactory + { + private readonly IStorage _storage; + private readonly Func, Func, Command> _commandBuilder; + private readonly Func, Func, Command>, IStorage, NodeName, List, ITreeNodeViewModel> _dirFactory; + private readonly Func, Func, Command>, IStorage, NodeName, ITreeNodeViewModel> _fileFactory; + + public NodeFactory( + IStorage storage, + Func, Func, Command>, IStorage, NodeName, List, ITreeNodeViewModel> dirFactory, + Func, Func, Command>, IStorage, NodeName, ITreeNodeViewModel> fileFactory, + Func, Func, Command> commandBuilder) + { + _storage = storage; + _commandBuilder = commandBuilder; + _dirFactory = dirFactory; + _fileFactory = fileFactory; + } + + public ITreeNodeViewModel CreateDir(NodeName name, List children) => + _dirFactory(_commandBuilder, _storage, name, children); + + public ITreeNodeViewModel CreateFile(NodeName name) => + _fileFactory(_commandBuilder, _storage, name); + } + + class TreeCompressor + { + public TreeCompressor( + Func, ITreeNodeViewModel> dirFactory, + Func fileFactory) + { + DirFactory = dirFactory; + FileFactory = fileFactory; + } + + public Func, ITreeNodeViewModel> DirFactory { get; } + + public Func FileFactory { get; } + } + + interface IFileDuplicates + { + int Id { get; } + + string Name { get; } + } + + class FileDuplicates : IFileDuplicates + { + public FileDuplicates(int id, string name) + { + Id = id; + Name = name; + } + + public int Id { get; } + + public string Name { get; } + } + + interface IFileDuplicatesViewModel + { + int Id { get; } + + string Name { get; } + } + + class FileDuplicatesViewModel : IFileDuplicatesViewModel + { + public FileDuplicatesViewModel( + IFileDuplicates duplicates, + NodeFactory nodeFactory, + TreeCompressor treeCompressor) + { + Id = duplicates.Id; + Name = duplicates.Name; + } + + public int Id { get; } + + public string Name { get; } + } + + class DuplicatesViewModel + { + public DuplicatesViewModel(Func factory) + { + Factory = factory; + } + + public Func Factory { get; } + } + + static class Setup + { + private static void SetupComposition() + { + DI.Setup(nameof(Composition)) + .Hint(Hint.Resolve, "Off") + .Bind().As(Lifetime.Singleton).To() + .Bind().As(Lifetime.Singleton).To() + .Bind().As(Lifetime.Singleton).To, Func, Command>>(ctx => + { + ctx.Inject(out IDispatcher dispatcher); + return (canExecute, execute) => + new Command(canExecute, execute) { Dispatcher = dispatcher }; + }) + .Singleton() + .Transient() + .Transient() + .Bind().To() + .Bind().As(Lifetime.Singleton).To() + .Root("Root"); + } + } + + public class Program + { + public static void Main() + { + var composition = new Composition(); + var root = composition.Root; + var vm = root.Factory(new FileDuplicates(42, "custom")); + Console.WriteLine($"{vm.Id} {vm.Name}"); + } + } + } + """.RunAsync(); + + // Then + result.Success.ShouldBeTrue(result); + result.StdOut.ShouldBe(["42 custom"], result); + } + [Fact] public async Task ShouldSupportStdFuncWithArg() { From 5ba74168f61f751b1d6aed3092056401bcccf7ca Mon Sep 17 00:00:00 2001 From: Nikolay Pianikov Date: Fri, 20 Feb 2026 17:25:57 +0300 Subject: [PATCH 02/15] Do not use local function when there are overrides --- src/Pure.DI.Core/Core/Code/LocalFunctions.cs | 49 +++++++++++++++++++- src/Pure.DI.Core/Core/GraphOverrider.cs | 3 +- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/Pure.DI.Core/Core/Code/LocalFunctions.cs b/src/Pure.DI.Core/Core/Code/LocalFunctions.cs index 30cdc4ffd..a91bec755 100644 --- a/src/Pure.DI.Core/Core/Code/LocalFunctions.cs +++ b/src/Pure.DI.Core/Core/Code/LocalFunctions.cs @@ -1,13 +1,58 @@ -namespace Pure.DI.Core.Code; +using Pure.DI.Core.Models; + +namespace Pure.DI.Core.Code; class LocalFunctions(INodeTools nodeTools): ILocalFunctions { public bool UseFor(CodeContext ctx) { + if (ctx.IsFactory && HasOverridesInDependencies(ctx)) + { + return false; + } + var var = ctx.VarInjection.Var; return ctx is { HasOverrides: false, Accumulators.Length: 0 } && nodeTools.IsBlock(var.AbstractNode) && ctx.RootContext.Graph.Graph.TryGetOutEdges(var.Declaration.Node.Node, out var targets) && targets.Count > 1; } -} \ No newline at end of file + + private static bool HasOverridesInDependencies(CodeContext ctx) + { + var graph = ctx.RootContext.Graph.Graph; + var visited = new HashSet(); + var stack = new Stack(); + stack.Push(ctx.VarInjection.Var.AbstractNode.Node); + while (stack.Count > 0) + { + var node = stack.Pop(); + if (!visited.Add(node.Binding.Id)) + { + continue; + } + + if (node.Factory?.HasOverrides == true) + { + return true; + } + + if (!graph.TryGetInEdges(node, out var dependencies)) + { + continue; + } + + foreach (var dependency in dependencies) + { + if (dependency.Injection.Kind is InjectionKind.FactoryInjection or InjectionKind.Override) + { + return true; + } + + stack.Push(dependency.Source); + } + } + + return false; + } +} diff --git a/src/Pure.DI.Core/Core/GraphOverrider.cs b/src/Pure.DI.Core/Core/GraphOverrider.cs index c8a87aa0e..4b9665b00 100644 --- a/src/Pure.DI.Core/Core/GraphOverrider.cs +++ b/src/Pure.DI.Core/Core/GraphOverrider.cs @@ -179,12 +179,13 @@ private DependencyNode Override( var currentDependency = dependency with { Target = targetNode }; if (!localNodesMap.TryGetValue(currentDependency.Injection, out var overridingSourceNode)) { + var sourceOverrides = overridesMap.ToDictionary(); var source = Override( processed, nodesMap, nextLocalOverrides, nextLocalOverrides.Count > 0, - overridesMap, + sourceOverrides, setup, graph, rootNode, From ce516cceee969fabc6bb8939b4d855636e396642 Mon Sep 17 00:00:00 2001 From: Nikolay Pianikov Date: Fri, 20 Feb 2026 18:57:29 +0300 Subject: [PATCH 03/15] Fix override key matching for func argument overrides --- src/Pure.DI.Core/Core/OverrideIdProvider.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Pure.DI.Core/Core/OverrideIdProvider.cs b/src/Pure.DI.Core/Core/OverrideIdProvider.cs index 18d148cc4..e8ce57e2a 100644 --- a/src/Pure.DI.Core/Core/OverrideIdProvider.cs +++ b/src/Pure.DI.Core/Core/OverrideIdProvider.cs @@ -23,10 +23,19 @@ public override bool Equals(object? obj) if (obj.GetType() != GetType()) return false; var other = (Key)obj; return SymbolEqualityComparer.Default.Equals(_type, other._type) - && (_tags.Count == 0 && other._tags.Count == 0 || _tags.Intersect(other._tags).Any()); + && _tags.SetEquals(other._tags); } - public override int GetHashCode() => - SymbolEqualityComparer.Default.GetHashCode(_type); + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(_type, SymbolEqualityComparer.Default); + foreach (var tag in _tags.OrderBy(i => i.GetHashCode())) + { + hash.Add(tag); + } + + return hash.ToHashCode(); + } } -} \ No newline at end of file +} From ac6691b7dd17f0c9ea859f9a3cbd15dc6346ccbe Mon Sep 17 00:00:00 2001 From: Nikolay Pianikov Date: Fri, 20 Feb 2026 19:02:14 +0300 Subject: [PATCH 04/15] Fix OverrideIdProvider hash for older target frameworks --- src/Pure.DI.Core/Core/OverrideIdProvider.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Pure.DI.Core/Core/OverrideIdProvider.cs b/src/Pure.DI.Core/Core/OverrideIdProvider.cs index e8ce57e2a..75e178433 100644 --- a/src/Pure.DI.Core/Core/OverrideIdProvider.cs +++ b/src/Pure.DI.Core/Core/OverrideIdProvider.cs @@ -28,14 +28,16 @@ public override bool Equals(object? obj) public override int GetHashCode() { - var hash = new HashCode(); - hash.Add(_type, SymbolEqualityComparer.Default); - foreach (var tag in _tags.OrderBy(i => i.GetHashCode())) + var hashCode = SymbolEqualityComparer.Default.GetHashCode(_type); + foreach (var tagHashCode in _tags.Select(GetTagHashCode).OrderBy(i => i)) { - hash.Add(tag); + hashCode = (hashCode * 397) ^ tagHashCode; } - return hash.ToHashCode(); + return hashCode; } + + private static int GetTagHashCode(object tag) => + tag.GetHashCode(); } } From fee5b5667e069630181c61a002fddcf2fc2d9872 Mon Sep 17 00:00:00 2001 From: Nikolay Pianikov Date: Fri, 20 Feb 2026 19:09:12 +0300 Subject: [PATCH 05/15] Add regression test for Func with duplicate argument types --- .../Pure.DI.IntegrationTests/OverrideTests.cs | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/tests/Pure.DI.IntegrationTests/OverrideTests.cs b/tests/Pure.DI.IntegrationTests/OverrideTests.cs index ba1910247..7f30b708d 100644 --- a/tests/Pure.DI.IntegrationTests/OverrideTests.cs +++ b/tests/Pure.DI.IntegrationTests/OverrideTests.cs @@ -2833,6 +2833,103 @@ public static void Main() result.StdOut.ShouldBe(["3", "a", "0", "b", "1", "c", "2"], result); } + [Fact] + public async Task ShouldSupportStdFuncWith2ArgsOfSameType() + { + // Given + + // When + var result = await """ + using System; + using System.Collections.Generic; + using Pure.DI; + + namespace Sample + { + interface IClock + { + DateTimeOffset Now { get; } + } + + class Clock : IClock + { + public DateTimeOffset Now => DateTimeOffset.Now; + } + + interface IDependency + { + int Id { get; } + int SubId { get; } + } + + class Dependency : IDependency + { + private readonly int _id; + private readonly int _subId; + + public Dependency(IClock clock, int id, [Tag("sub")] int subId) + { + _id = id; + _subId = subId; + } + + public int Id => _id; + + public int SubId => _subId; + } + + interface IService + { + List Dependencies { get; } + } + + class Service : IService + { + public Service(Func dependencyFactory) + { + Dependencies = new List + { + dependencyFactory(10, 100), + dependencyFactory(11, 101), + dependencyFactory(12, 102) + }; + } + + public List Dependencies { get; } + } + + static class Setup + { + private static void SetupComposition() + { + DI.Setup(nameof(Composition)) + .Bind().As(Lifetime.Singleton).To() + .Bind().To() + .Bind().To() + .Root("Root"); + } + } + + public class Program + { + public static void Main() + { + var composition = new Composition(); + var service = composition.Root; + Console.WriteLine(service.Dependencies.Count); + Console.WriteLine($"{service.Dependencies[0].Id}:{service.Dependencies[0].SubId}"); + Console.WriteLine($"{service.Dependencies[1].Id}:{service.Dependencies[1].SubId}"); + Console.WriteLine($"{service.Dependencies[2].Id}:{service.Dependencies[2].SubId}"); + } + } + } + """.RunAsync(); + + // Then + result.Success.ShouldBeTrue(result); + result.StdOut.ShouldBe(["3", "10:100", "11:101", "12:102"], result); + } + [Fact] public async Task ShouldSupportOverrideWhenCtor() { From fa9b9d6fa7c23c13af24f3d33df6f57c9bc38f99 Mon Sep 17 00:00:00 2001 From: Nikolay Pianikov Date: Fri, 20 Feb 2026 19:21:13 +0300 Subject: [PATCH 06/15] Adjust duplicate Func arg test to assert current unresolved-tag behavior --- tests/Pure.DI.IntegrationTests/OverrideTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Pure.DI.IntegrationTests/OverrideTests.cs b/tests/Pure.DI.IntegrationTests/OverrideTests.cs index 7f30b708d..d61b9a72c 100644 --- a/tests/Pure.DI.IntegrationTests/OverrideTests.cs +++ b/tests/Pure.DI.IntegrationTests/OverrideTests.cs @@ -2834,7 +2834,7 @@ public static void Main() } [Fact] - public async Task ShouldSupportStdFuncWith2ArgsOfSameType() + public async Task ShouldReportErrorForStdFuncWith2ArgsOfSameTypeAndTag() { // Given @@ -2926,8 +2926,8 @@ public static void Main() """.RunAsync(); // Then - result.Success.ShouldBeTrue(result); - result.StdOut.ShouldBe(["3", "10:100", "11:101", "12:102"], result); + result.Success.ShouldBeFalse(result); + result.Errors.Count(i => i.Id == LogId.ErrorUnableToResolve && i.Text.Contains("int(\"sub\")")).ShouldBe(1, result); } [Fact] From 9579a0608ba2a7ca401947307a6077b08d612390 Mon Sep 17 00:00:00 2001 From: Nikolay Pianikov Date: Fri, 20 Feb 2026 19:25:10 +0300 Subject: [PATCH 07/15] Use manual override test for duplicate Func arg types with tags --- tests/Pure.DI.IntegrationTests/OverrideTests.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/Pure.DI.IntegrationTests/OverrideTests.cs b/tests/Pure.DI.IntegrationTests/OverrideTests.cs index d61b9a72c..34554ec6a 100644 --- a/tests/Pure.DI.IntegrationTests/OverrideTests.cs +++ b/tests/Pure.DI.IntegrationTests/OverrideTests.cs @@ -2834,7 +2834,7 @@ public static void Main() } [Fact] - public async Task ShouldReportErrorForStdFuncWith2ArgsOfSameTypeAndTag() + public async Task ShouldSupportOverrideWhenFuncWith2ArgsOfSameTypeAndTag() { // Given @@ -2904,7 +2904,14 @@ private static void SetupComposition() { DI.Setup(nameof(Composition)) .Bind().As(Lifetime.Singleton).To() - .Bind().To() + .Bind().To>(ctx => + (id, subId) => + { + ctx.Override(id); + ctx.Override(subId, "sub"); + ctx.Inject(out var dependency); + return dependency; + }) .Bind().To() .Root("Root"); } @@ -2926,8 +2933,8 @@ public static void Main() """.RunAsync(); // Then - result.Success.ShouldBeFalse(result); - result.Errors.Count(i => i.Id == LogId.ErrorUnableToResolve && i.Text.Contains("int(\"sub\")")).ShouldBe(1, result); + result.Success.ShouldBeTrue(result); + result.StdOut.ShouldBe(["3", "10:100", "11:101", "12:102"], result); } [Fact] From 8a5019518b59f5418cd7240ee5419ed5fa2e4693 Mon Sep 17 00:00:00 2001 From: Nikolay Pianikov Date: Fri, 20 Feb 2026 19:37:49 +0300 Subject: [PATCH 08/15] Fix override arg var leakage across nested lazy factories --- src/Pure.DI.Core/Core/Code/VarsMap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Pure.DI.Core/Core/Code/VarsMap.cs b/src/Pure.DI.Core/Core/Code/VarsMap.cs index b1016b10d..fb3d323b1 100644 --- a/src/Pure.DI.Core/Core/Code/VarsMap.cs +++ b/src/Pure.DI.Core/Core/Code/VarsMap.cs @@ -194,7 +194,7 @@ private void RemoveNewNonPersistentVars(Var var, IReadOnlyDictionary Date: Fri, 20 Feb 2026 19:47:08 +0300 Subject: [PATCH 09/15] Clean up current non-persistent vars after lazy/local scopes --- src/Pure.DI.Core/Core/Code/VarsMap.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Pure.DI.Core/Core/Code/VarsMap.cs b/src/Pure.DI.Core/Core/Code/VarsMap.cs index fb3d323b1..62c3255d5 100644 --- a/src/Pure.DI.Core/Core/Code/VarsMap.cs +++ b/src/Pure.DI.Core/Core/Code/VarsMap.cs @@ -189,12 +189,14 @@ private void RemoveNewNonPersistentVars(Var var, IReadOnlyDictionary Date: Fri, 20 Feb 2026 19:52:13 +0300 Subject: [PATCH 10/15] Exclude override construct vars from scope snapshots --- src/Pure.DI.Core/Core/Code/VarsMap.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Pure.DI.Core/Core/Code/VarsMap.cs b/src/Pure.DI.Core/Core/Code/VarsMap.cs index 62c3255d5..6b7a07068 100644 --- a/src/Pure.DI.Core/Core/Code/VarsMap.cs +++ b/src/Pure.DI.Core/Core/Code/VarsMap.cs @@ -170,7 +170,8 @@ private VarDeclaration CreateDeclaration(IDependencyNode node) => /// private IReadOnlyDictionary CreateState(Var var) => _map - .Where(i => i.Key != var.Declaration.Node.BindingId) + .Where(i => i.Key != var.Declaration.Node.BindingId + && i.Value.Declaration.Node.Construct is not { Source.Kind: MdConstructKind.Override }) .ToDictionary(i => i.Key, i => new VarState(i.Value)); /// From dadf623073f5f16796561fa10250328a5b314cc4 Mon Sep 17 00:00:00 2001 From: Nikolay Pianikov Date: Fri, 20 Feb 2026 20:00:28 +0300 Subject: [PATCH 11/15] Avoid override-context cache reuse in GraphOverrider --- src/Pure.DI.Core/Core/GraphOverrider.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Pure.DI.Core/Core/GraphOverrider.cs b/src/Pure.DI.Core/Core/GraphOverrider.cs index 4b9665b00..548df40d0 100644 --- a/src/Pure.DI.Core/Core/GraphOverrider.cs +++ b/src/Pure.DI.Core/Core/GraphOverrider.cs @@ -67,7 +67,13 @@ private DependencyNode Override( return targetNode; } - if (processed.TryGetValue(targetNode.Binding.Id, out var node)) + // Rewritten nodes are context-dependent when any override scope is active. + // In such cases we must not reuse a node cached only by Binding.Id, + // otherwise local override branches can leak into sibling branches. + var canUseProcessedCache = !consumeLocalOverrides + && localOverrides.Count == 0 + && overrides.Count == 0; + if (canUseProcessedCache && processed.TryGetValue(targetNode.Binding.Id, out var node)) { return node; } @@ -98,7 +104,10 @@ private DependencyNode Override( overridesEnumerable = []; } - processed.Add(targetNode.Binding.Id, targetNode); + if (canUseProcessedCache) + { + processed[targetNode.Binding.Id] = targetNode; + } var newDependencies = new List(dependencies.Count); var lastDependencyPosition = 0; using var overridesEnumerator = overridesEnumerable.GetEnumerator(); From 781883356147e0a4b092343980bbab99a8ee2bb0 Mon Sep 17 00:00:00 2001 From: Nikolay Pianikov Date: Fri, 20 Feb 2026 20:17:28 +0300 Subject: [PATCH 12/15] Refine GraphOverrider cache guard for deep override context --- src/Pure.DI.Core/Core/GraphOverrider.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Pure.DI.Core/Core/GraphOverrider.cs b/src/Pure.DI.Core/Core/GraphOverrider.cs index 548df40d0..238c5bd64 100644 --- a/src/Pure.DI.Core/Core/GraphOverrider.cs +++ b/src/Pure.DI.Core/Core/GraphOverrider.cs @@ -69,8 +69,9 @@ private DependencyNode Override( // Rewritten nodes are context-dependent when any override scope is active. // In such cases we must not reuse a node cached only by Binding.Id, - // otherwise local override branches can leak into sibling branches. + // otherwise override branches can leak into sibling branches. var canUseProcessedCache = !consumeLocalOverrides + && nodes.Count == 0 && localOverrides.Count == 0 && overrides.Count == 0; if (canUseProcessedCache && processed.TryGetValue(targetNode.Binding.Id, out var node)) From 60d2a7f833ee18a91826a1c20bb0cda355b882df Mon Sep 17 00:00:00 2001 From: Nikolay Pianikov Date: Fri, 20 Feb 2026 20:17:33 +0300 Subject: [PATCH 13/15] Fix GraphOverrider recursion with branch-scoped memoization --- src/Pure.DI.Core/Core/GraphOverrider.cs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/Pure.DI.Core/Core/GraphOverrider.cs b/src/Pure.DI.Core/Core/GraphOverrider.cs index 238c5bd64..e08997784 100644 --- a/src/Pure.DI.Core/Core/GraphOverrider.cs +++ b/src/Pure.DI.Core/Core/GraphOverrider.cs @@ -68,13 +68,14 @@ private DependencyNode Override( } // Rewritten nodes are context-dependent when any override scope is active. - // In such cases we must not reuse a node cached only by Binding.Id, - // otherwise override branches can leak into sibling branches. - var canUseProcessedCache = !consumeLocalOverrides - && nodes.Count == 0 - && localOverrides.Count == 0 - && overrides.Count == 0; - if (canUseProcessedCache && processed.TryGetValue(targetNode.Binding.Id, out var node)) + // In such cases we isolate memoization to the current branch to avoid + // leaking context-dependent rewrites into sibling branches. + var isContextFree = !consumeLocalOverrides + && nodes.Count == 0 + && localOverrides.Count == 0 + && overrides.Count == 0; + var branchProcessed = isContextFree ? processed : processed.ToDictionary(); + if (branchProcessed.TryGetValue(targetNode.Binding.Id, out var node)) { return node; } @@ -105,10 +106,7 @@ private DependencyNode Override( overridesEnumerable = []; } - if (canUseProcessedCache) - { - processed[targetNode.Binding.Id] = targetNode; - } + branchProcessed[targetNode.Binding.Id] = targetNode; var newDependencies = new List(dependencies.Count); var lastDependencyPosition = 0; using var overridesEnumerator = overridesEnumerable.GetEnumerator(); @@ -191,7 +189,7 @@ private DependencyNode Override( { var sourceOverrides = overridesMap.ToDictionary(); var source = Override( - processed, + branchProcessed, nodesMap, nextLocalOverrides, nextLocalOverrides.Count > 0, From a290b313a4167183595d6537a72d71f9206fa01e Mon Sep 17 00:00:00 2001 From: Nikolay Pianikov Date: Fri, 20 Feb 2026 20:26:12 +0300 Subject: [PATCH 14/15] Deduplicate GraphOverrider entries by target node --- src/Pure.DI.Core/Core/GraphOverrider.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Pure.DI.Core/Core/GraphOverrider.cs b/src/Pure.DI.Core/Core/GraphOverrider.cs index e08997784..18e733d1c 100644 --- a/src/Pure.DI.Core/Core/GraphOverrider.cs +++ b/src/Pure.DI.Core/Core/GraphOverrider.cs @@ -215,7 +215,17 @@ private DependencyNode Override( newDependencies.Add(currentDependency); } - entries.Add(new GraphEntry(targetNode, newDependencies)); + var entry = new GraphEntry(targetNode, newDependencies); + var entryIndex = entries.FindIndex(i => Equals(i.Target, targetNode)); + if (entryIndex >= 0) + { + entries[entryIndex] = entry; + } + else + { + entries.Add(entry); + } + return targetNode; } } From 9844a969e4e33185f0d60e233f339f0efcbf334b Mon Sep 17 00:00:00 2001 From: Nikolay Pianikov Date: Fri, 20 Feb 2026 20:10:05 +0300 Subject: [PATCH 15/15] Fix test, minor --- src/Pure.DI.Core/Core/GraphOverrider.cs | 2 +- tests/Pure.DI.IntegrationTests/OverrideTests.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Pure.DI.Core/Core/GraphOverrider.cs b/src/Pure.DI.Core/Core/GraphOverrider.cs index 18e733d1c..c7378504d 100644 --- a/src/Pure.DI.Core/Core/GraphOverrider.cs +++ b/src/Pure.DI.Core/Core/GraphOverrider.cs @@ -74,7 +74,7 @@ private DependencyNode Override( && nodes.Count == 0 && localOverrides.Count == 0 && overrides.Count == 0; - var branchProcessed = isContextFree ? processed : processed.ToDictionary(); + var branchProcessed = isContextFree ? processed : new Dictionary(processed); if (branchProcessed.TryGetValue(targetNode.Binding.Id, out var node)) { return node; diff --git a/tests/Pure.DI.IntegrationTests/OverrideTests.cs b/tests/Pure.DI.IntegrationTests/OverrideTests.cs index 34554ec6a..a2cf2c6a7 100644 --- a/tests/Pure.DI.IntegrationTests/OverrideTests.cs +++ b/tests/Pure.DI.IntegrationTests/OverrideTests.cs @@ -2616,7 +2616,6 @@ private static void SetupComposition() new Command(canExecute, execute) { Dispatcher = dispatcher }; }) .Singleton() - .Transient() .Transient() .Bind().To() .Bind().As(Lifetime.Singleton).To()