From dc38e5ffc293ac4f318edd026942c1c656d2f7cb Mon Sep 17 00:00:00 2001 From: Nikolay Pianikov Date: Fri, 22 Mar 2024 12:43:04 +0300 Subject: [PATCH] #44 Disposable Instances Handling --- readme/accumulators.md | 12 +- ...cking-disposable-instances-in-delegates.md | 1 - ...osable-instances-per-a-composition-root.md | 1 - src/Pure.DI.Core/Core/AccumulatorKey.cs | 23 -- src/Pure.DI.Core/Core/Code/Accumulator.cs | 1 + .../Core/Code/BlockCodeBuilder.cs | 7 +- src/Pure.DI.Core/Core/Code/BuildTools.cs | 20 +- .../Core/Code/VariablesBuilder.cs | 19 +- .../Core/DependencyGraphBuilder.cs | 39 ++-- src/Pure.DI.Core/Core/Models/MdConstruct.cs | 3 +- .../AccumulatorTests.cs | 202 ++++++++++++++++-- .../Advanced/AccumulatorScenario.cs | 16 +- .../Advanced/TagTypeScenario.cs | 2 - 13 files changed, 253 insertions(+), 93 deletions(-) delete mode 100644 src/Pure.DI.Core/Core/AccumulatorKey.cs diff --git a/readme/accumulators.md b/readme/accumulators.md index 7624976c2..9d1ada5c4 100644 --- a/readme/accumulators.md +++ b/readme/accumulators.md @@ -5,7 +5,7 @@ Accumulators allow you to accumulate instances of certain types and lifetimes. ```c# -interface IAccumulating { } +interface IAccumulating; class MyAccumulator: List; @@ -48,13 +48,10 @@ DI.Setup(nameof(Composition)) .Root<(IService service, MyAccumulator accumulator)>("Root"); var composition = new Composition(); -var root = composition.Root; -var service = root.service; -var accumulator = root.accumulator; -accumulator.Count.ShouldBe(3); +var (service, accumulator) = composition.Root; +accumulator.Count.ShouldBe(2); accumulator[0].ShouldBeOfType(); -accumulator[1].ShouldBeOfType(); -accumulator[2].ShouldBeOfType(); +accumulator[1].ShouldBeOfType(); ``` @@ -130,7 +127,6 @@ partial class Composition { var accumulatorM03D22di41 = new Pure.DI.UsageTests.Advanced.AccumulatorScenario.MyAccumulator(); Pure.DI.UsageTests.Advanced.AccumulatorScenario.AbcDependency perBlockM03D22di4_AbcDependency = new Pure.DI.UsageTests.Advanced.AccumulatorScenario.AbcDependency(); - accumulatorM03D22di41.Add(perBlockM03D22di4_AbcDependency); if (ReferenceEquals(_rootM03D22di._singletonM03D22di38_XyzDependency, null)) { lock (_lockM03D22di) diff --git a/readme/tracking-disposable-instances-in-delegates.md b/readme/tracking-disposable-instances-in-delegates.md index 98e5f0a49..a5458606b 100644 --- a/readme/tracking-disposable-instances-in-delegates.md +++ b/readme/tracking-disposable-instances-in-delegates.md @@ -134,7 +134,6 @@ partial class Composition var value_M03D22di3 = transientM03D22di3_Dependency; perBlockM03D22di1_Owned = new Owned(value_M03D22di3, owned_M03D22di2); } - accumulatorM03D22di42.Add(perBlockM03D22di1_Owned); var factory_M03D22di1 = perBlockM03D22di1_Owned; return factory_M03D22di1; }); diff --git a/readme/tracking-disposable-instances-per-a-composition-root.md b/readme/tracking-disposable-instances-per-a-composition-root.md index af4dff7f3..291e3ddcf 100644 --- a/readme/tracking-disposable-instances-per-a-composition-root.md +++ b/readme/tracking-disposable-instances-per-a-composition-root.md @@ -121,7 +121,6 @@ partial class Composition var value_M03D22di2 = new Pure.DI.UsageTests.Basics.TrackingDisposableScenario.Service(transientM03D22di3_Dependency); perBlockM03D22di0_Owned = new Owned(value_M03D22di2, owned_M03D22di1); } - accumulatorM03D22di39.Add(perBlockM03D22di0_Owned); return perBlockM03D22di0_Owned; } } diff --git a/src/Pure.DI.Core/Core/AccumulatorKey.cs b/src/Pure.DI.Core/Core/AccumulatorKey.cs deleted file mode 100644 index 2f4230e60..000000000 --- a/src/Pure.DI.Core/Core/AccumulatorKey.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Pure.DI.Core; - -internal readonly struct AccumulatorKey( - ITypeSymbol type, - Lifetime lifetime) -{ - private readonly ITypeSymbol _type = type; - private readonly Lifetime _lifetime = lifetime; - - public override bool Equals(object? obj) => - obj is AccumulatorKey other && Equals(other); - - private bool Equals(AccumulatorKey other) => - SymbolEqualityComparer.Default.Equals(_type, other._type) && _lifetime == other._lifetime; - - public override int GetHashCode() - { - unchecked - { - return (SymbolEqualityComparer.Default.GetHashCode(_type) * 397) ^ (int)_lifetime; - } - } -} \ No newline at end of file diff --git a/src/Pure.DI.Core/Core/Code/Accumulator.cs b/src/Pure.DI.Core/Core/Code/Accumulator.cs index ab3b13a0a..23f0e188d 100644 --- a/src/Pure.DI.Core/Core/Code/Accumulator.cs +++ b/src/Pure.DI.Core/Core/Code/Accumulator.cs @@ -5,4 +5,5 @@ internal record Accumulator( string Name, bool IsDeclared, ITypeSymbol Type, + Lifetime Lifetime, ITypeSymbol AccumulatorType); \ No newline at end of file diff --git a/src/Pure.DI.Core/Core/Code/BlockCodeBuilder.cs b/src/Pure.DI.Core/Core/Code/BlockCodeBuilder.cs index 27ba4a8a7..3afbb2c9d 100644 --- a/src/Pure.DI.Core/Core/Code/BlockCodeBuilder.cs +++ b/src/Pure.DI.Core/Core/Code/BlockCodeBuilder.cs @@ -36,19 +36,18 @@ variable.Node.Lifetime is Lifetime.Singleton or Lifetime.Scoped { parent = $"{Names.ParentFieldName}."; } - - var accumulators = new List(); + var uniqueAccumulators = ctx.Accumulators .Where(accumulator => !accumulator.IsDeclared) - .GroupBy(i => i.AccumulatorType, SymbolEqualityComparer.Default) + .GroupBy(i => i.Name) .Select(i => i.First()); foreach (var accumulator in uniqueAccumulators) { code.AppendLine($"var {accumulator.Name} = new {accumulator.AccumulatorType}();"); - accumulators.Add(accumulator with { IsDeclared = true }); } + var accumulators = ctx.Accumulators.Select(accumulator => accumulator with { IsDeclared = true }).ToList(); if (accumulators.Count > 0) { ctx = ctx with { Accumulators = accumulators.ToImmutableArray() }; diff --git a/src/Pure.DI.Core/Core/Code/BuildTools.cs b/src/Pure.DI.Core/Core/Code/BuildTools.cs index 26c623426..07bdfab78 100644 --- a/src/Pure.DI.Core/Core/Code/BuildTools.cs +++ b/src/Pure.DI.Core/Core/Code/BuildTools.cs @@ -54,6 +54,9 @@ public IEnumerable OnCreated(BuildContext ctx, Variable variable) var lines = ctx.Accumulators .Where(i => FilterAccumulator(i, variable.Node.Lifetime)) .Where(i => baseTypes.Contains(i.Type)) + .GroupBy(i => i.Name) + .Select(i => i.First()) + .OrderBy(i => i.Name) .Select(i => new Line(0, $"{i.Name}.Add({variable.VariableName});")) .ToList(); @@ -76,9 +79,20 @@ public IEnumerable OnCreated(BuildContext ctx, Variable variable) return lines; } - private static bool FilterAccumulator(Accumulator accumulator, Lifetime lifetime) => - accumulator.IsRoot - || lifetime is not (Lifetime.Singleton or Lifetime.Scoped or Lifetime.PerResolve); + private static bool FilterAccumulator(Accumulator accumulator, Lifetime lifetime) + { + if (accumulator.Lifetime != lifetime) + { + return false; + } + + if (accumulator.IsRoot) + { + return true; + } + + return lifetime is not (Lifetime.Singleton or Lifetime.Scoped or Lifetime.PerResolve); + } private static object? GetTag(BuildContext ctx, Variable variable) { diff --git a/src/Pure.DI.Core/Core/Code/VariablesBuilder.cs b/src/Pure.DI.Core/Core/Code/VariablesBuilder.cs index e82634b71..e6cf491b7 100644 --- a/src/Pure.DI.Core/Core/Code/VariablesBuilder.cs +++ b/src/Pure.DI.Core/Core/Code/VariablesBuilder.cs @@ -36,7 +36,7 @@ public Block Build( case Variable variable: { - var isAccumulator = IsAccumulator(variable, out var construct); + var isAccumulator = IsAccumulator(variable, out var construct, out var mdAccumulators); IReadOnlyCollection dependencies = Array.Empty(); if (!isAccumulator) { @@ -73,7 +73,18 @@ public Block Build( accumulators ??= rootNode.Accumulators; if (isAccumulator) { - accumulators.Add(new Accumulator(isRoot, GetAccumulatorName(variable), false, construct.ElementType, construct.Type)); + var name = GetAccumulatorName(variable); + foreach (var mdAccumulator in mdAccumulators) + { + accumulators.Add( + new Accumulator( + isRoot, + name, + false, + construct.ElementType, + mdAccumulator.Lifetime, + construct.Type)); + } } foreach (var (isDepResolved, depNode, depInjection, _) in dependencies) @@ -139,15 +150,17 @@ public Block Build( return rootBlock; } - private static bool IsAccumulator(Variable variable, out MdConstruct mdConstruct) + private static bool IsAccumulator(Variable variable, out MdConstruct mdConstruct, out IReadOnlyCollection accumulators) { if(variable.Node.Construct?.Source is { Kind: MdConstructKind.Accumulator } construct) { mdConstruct = construct; + accumulators = construct.State as IReadOnlyCollection ?? ImmutableArray.Empty; return true; } mdConstruct = default; + accumulators = ImmutableArray.Empty; return false; } diff --git a/src/Pure.DI.Core/Core/DependencyGraphBuilder.cs b/src/Pure.DI.Core/Core/DependencyGraphBuilder.cs index ed9ae2789..334afecbd 100644 --- a/src/Pure.DI.Core/Core/DependencyGraphBuilder.cs +++ b/src/Pure.DI.Core/Core/DependencyGraphBuilder.cs @@ -2,6 +2,7 @@ // ReSharper disable InvertIf // ReSharper disable ClassNeverInstantiated.Global // ReSharper disable ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator +// ReSharper disable IdentifierTypo namespace Pure.DI.Core; internal sealed class DependencyGraphBuilder( @@ -52,10 +53,16 @@ public IEnumerable TryBuild( } } - var accumulators = new Dictionary(); + var accumulators = new Dictionary>(SymbolEqualityComparer.Default); foreach (var accumulator in setup.Accumulators) { - accumulators[new AccumulatorKey(accumulator.AccumulatorType, accumulator.Lifetime)] = accumulator; + if (!accumulators.TryGetValue(accumulator.AccumulatorType, out var accs)) + { + accs = []; + accumulators.Add(accumulator.AccumulatorType, accs); + } + + accs.Add(accumulator); } var processed = new HashSet(); @@ -76,13 +83,13 @@ public IEnumerable TryBuild( } } - if (accumulators.TryGetValue(new AccumulatorKey(injection.Type, node.Node.Lifetime), out var accumulator)) + if (accumulators.TryGetValue(injection.Type, out var accs)) { var accumulatorBinding = CreateAccumulatorBinding( setup, targetNode, ref maxId, - accumulator, + accs, hasExplicitDefaultValue, explicitDefaultValue); @@ -143,16 +150,6 @@ public IEnumerable TryBuild( case MdConstructKind.None: break; - case MdConstructKind.Accumulator: - var accumulatorBinding = CreateAccumulatorBinding( - setup, - targetNode, - ref maxId, - accumulator, - hasExplicitDefaultValue, - explicitDefaultValue); - return CreateNodes(setup, accumulatorBinding); - default: var lifetime = constructKind == MdConstructKind.Enumerable ? Lifetime.PerBlock : Lifetime.Transient; var constructBinding = CreateConstructBinding( @@ -439,10 +436,12 @@ private MdBinding CreateGenericBinding( private static MdBinding CreateAccumulatorBinding(MdSetup setup, DependencyNode targetNode, ref int maxId, - MdAccumulator accumulator, + IReadOnlyCollection accumulators, bool hasExplicitDefaultValue, - object? explicitDefaultValue) => - new( + object? explicitDefaultValue) + { + var accumulator = accumulators.First(); + return new MdBinding( ++maxId, targetNode.Binding.Source, setup, @@ -459,9 +458,11 @@ private static MdBinding CreateAccumulatorBinding(MdSetup setup, accumulator.AccumulatorType, accumulator.Type, MdConstructKind.Accumulator, - ImmutableArray.Empty, + ImmutableArray.Empty, hasExplicitDefaultValue, - explicitDefaultValue)); + explicitDefaultValue, + accumulators)); + } private MdBinding CreateAutoBinding( MdSetup setup, diff --git a/src/Pure.DI.Core/Core/Models/MdConstruct.cs b/src/Pure.DI.Core/Core/Models/MdConstruct.cs index c30a1d7b2..a32ef313c 100644 --- a/src/Pure.DI.Core/Core/Models/MdConstruct.cs +++ b/src/Pure.DI.Core/Core/Models/MdConstruct.cs @@ -9,7 +9,8 @@ internal readonly record struct MdConstruct( MdConstructKind Kind, ImmutableArray Dependencies, bool HasExplicitDefaultValue, - object? ExplicitDefaultValue) + object? ExplicitDefaultValue, + object? State = default) { public override string ToString() => $"To<{Kind}<{Type}>>()"; } \ No newline at end of file diff --git a/tests/Pure.DI.IntegrationTests/AccumulatorTests.cs b/tests/Pure.DI.IntegrationTests/AccumulatorTests.cs index d4fb86399..4879e6e5a 100644 --- a/tests/Pure.DI.IntegrationTests/AccumulatorTests.cs +++ b/tests/Pure.DI.IntegrationTests/AccumulatorTests.cs @@ -15,27 +15,51 @@ public async Task ShouldSupportAccumulator() namespace Sample { - interface IDependency {} + interface IAccumulating + { + } - class Dependency: IDependency {} + class MyAccumulator : List + { + } - interface IService + interface IDependency { - IDependency Dep { get; } } - class Service: IService + class AbcDependency : IDependency, IAccumulating { - public Service(IDependency dep) - { - Dep = dep; - } + } - public IDependency Dep { get; } + class XyzDependency : IDependency, IAccumulating + { } - - class DependencyAccumulator: List + + interface IService: IAccumulating + { + IDependency Dependency1 { get; } + + IDependency Dependency2 { get; } + + IDependency Dependency3 { get; } + } + + class Service : IService { + public Service([Tag(typeof(AbcDependency))] IDependency dependency1, + [Tag(typeof(XyzDependency))] IDependency dependency2, + IDependency dependency3) + { + Dependency1 = dependency1; + Dependency2 = dependency2; + Dependency3 = dependency3; + } + + public IDependency Dependency1 { get; } + + public IDependency Dependency2 { get; } + + public IDependency Dependency3 { get; } } static class Setup @@ -43,10 +67,12 @@ static class Setup private static void SetupComposition() { DI.Setup("Composition") - .Bind().To(ctx => new Dependency()) + .Accumulate(Lifetime.Transient, Lifetime.Singleton) + .Bind().As(Lifetime.PerBlock).To() + .Bind(Tag.Type).To() + .Bind(Tag.Type).As(Lifetime.Singleton).To() .Bind().To() - .Accumulate(Lifetime.Transient) - .Root<(IService service, DependencyAccumulator dependencies)>("Service"); + .Root<(IService service, MyAccumulator accumulator)>("Root"); } } @@ -55,8 +81,12 @@ public class Program public static void Main() { var composition = new Composition(); - var root = composition.Service; - var service = root.service; + var (service, accumulator) = composition.Root; + Console.WriteLine(accumulator.Count); + foreach(var val in accumulator) + { + Console.WriteLine(val); + } } } } @@ -64,6 +94,7 @@ public static void Main() // Then result.Success.ShouldBeTrue(result); + result.StdOut.ShouldBe(ImmutableArray.Create("3", "Sample.XyzDependency", "Sample.AbcDependency", "Sample.Service")); } [Fact] @@ -184,6 +215,141 @@ public static void Main() // Then result.Success.ShouldBeTrue(result); - result.StdOut.ShouldBe(ImmutableArray.Create("Value is not created.", "Sample.ShroedingersCat", "(Sample.ShroedingersCat, Sample.Accumulator)", "CardboardBox created", "(Sample.Program, Sample.Accumulator)", "System.Func`1[System.ValueTuple`2[Sample.ICat,Sample.Accumulator]]", "[Sample.ShroedingersCat]", "Sample.Program", "(Sample.Program, Sample.Accumulator)", "Program created")); + result.StdOut.ShouldBe(ImmutableArray.Create("Value is not created.", "Sample.ShroedingersCat", "(Sample.ShroedingersCat, Sample.Accumulator)", "CardboardBox created", "(Sample.Program, Sample.Accumulator)", "[Sample.ShroedingersCat]", "Sample.Program", "(Sample.Program, Sample.Accumulator)", "Program created")); + } + + [Fact] + public async Task ShouldSupportAccumulatorWhenLifetime() + { + // Given + + // When + var result = await """ +using System; +using System.Collections.Generic; +using Pure.DI; + +namespace Sample +{ + interface IDependency {} + + class Dependency: IDependency {} + + interface IService + { + IDependency Dep { get; } + } + + class Service: IService + { + public Service(IDependency dep) + { + Dep = dep; + } + + public IDependency Dep { get; } + } + + class MyAccumulator: List { } + + static class Setup + { + private static void SetupComposition() + { + DI.Setup("Composition") + .Bind().As(Lifetime.PerResolve).To(ctx => new Dependency()) + .Bind().To() + .Accumulate(Lifetime.Transient, Lifetime.PerBlock) + .Root<(IService service, MyAccumulator acc)>("Service"); + } + } + + public class Program + { + public static void Main() + { + var composition = new Composition(); + var root = composition.Service; + var service = root.service; + root.acc.ForEach(i => Console.WriteLine(i)); + } + } +} +""".RunAsync(); + + // Then + result.Success.ShouldBeTrue(result); + result.StdOut.ShouldBe(ImmutableArray.Create("Sample.Service", "(Sample.Service, Sample.MyAccumulator)")); + } + + [Fact] + public async Task ShouldSupportOwned() + { + // Given + + // When + var result = await """ + using System; + using System.Collections.Generic; + using Pure.DI; + + namespace Sample + { + interface IDependency + { + bool IsDisposed { get; } + } + + class Dependency : IDependency, IDisposable + { + public bool IsDisposed { get; private set; } + + public void Dispose() => IsDisposed = true; + } + + interface IService + { + public IDependency Dependency { get; } + } + + class Service : IService + { + public Service(IDependency dependency) + { + Dependency = dependency; + } + + public IDependency Dependency { get; } + } + + partial class Composition + { + private static void Setup() => + DI.Setup(nameof(Composition)) + .Bind().To() + .Bind().To() + .Root>("Root"); + } + + public class Program + { + public static void Main() + { + var composition = new Composition(); + var root1 = composition.Root; + var root2 = composition.Root; + root2.Dispose(); + Console.WriteLine(root2.Value.Dependency.IsDisposed); + Console.WriteLine(root1.Value.Dependency.IsDisposed); + root1.Dispose(); + Console.WriteLine(root1.Value.Dependency.IsDisposed); + } + } + } +""".RunAsync(); + + // Then + result.Success.ShouldBeTrue(result); + result.StdOut.ShouldBe(ImmutableArray.Create("True", "False", "True")); } } \ No newline at end of file diff --git a/tests/Pure.DI.UsageTests/Advanced/AccumulatorScenario.cs b/tests/Pure.DI.UsageTests/Advanced/AccumulatorScenario.cs index ac25967ae..a963682af 100644 --- a/tests/Pure.DI.UsageTests/Advanced/AccumulatorScenario.cs +++ b/tests/Pure.DI.UsageTests/Advanced/AccumulatorScenario.cs @@ -16,17 +16,15 @@ namespace Pure.DI.UsageTests.Advanced.AccumulatorScenario; using Xunit; // { -interface IAccumulating { } +interface IAccumulating; -class MyAccumulator: List; +class MyAccumulator : List; interface IDependency; class AbcDependency : IDependency, IAccumulating; - -class XyzDependency : IDependency; - -class Dependency : IDependency; + +class XyzDependency : IDependency, IAccumulating; interface IService: IAccumulating { @@ -66,11 +64,9 @@ public void Run() .Root<(IService service, MyAccumulator accumulator)>("Root"); var composition = new Composition(); - var root = composition.Root; - var service = root.service; - var accumulator = root.accumulator; + var (service, accumulator) = composition.Root; accumulator.Count.ShouldBe(3); - accumulator[0].ShouldBeOfType(); + accumulator[0].ShouldBeOfType(); accumulator[1].ShouldBeOfType(); accumulator[2].ShouldBeOfType(); diff --git a/tests/Pure.DI.UsageTests/Advanced/TagTypeScenario.cs b/tests/Pure.DI.UsageTests/Advanced/TagTypeScenario.cs index 4f4c8c4e8..e1894ee3f 100644 --- a/tests/Pure.DI.UsageTests/Advanced/TagTypeScenario.cs +++ b/tests/Pure.DI.UsageTests/Advanced/TagTypeScenario.cs @@ -21,8 +21,6 @@ interface IDependency; class AbcDependency : IDependency; class XyzDependency : IDependency; - -class Dependency : IDependency; interface IService {