From 03ee3d8a8ce8239b347f8337cda02298f6f41f2f Mon Sep 17 00:00:00 2001 From: Nikolay Pianikov Date: Wed, 20 Mar 2024 11:14:46 +0300 Subject: [PATCH] #44 Disposable Instances Handling --- src/Pure.DI.Core/Components/Api.g.cs | 75 ++++++++ .../Core/ApiInvocationProcessor.cs | 12 ++ src/Pure.DI.Core/Core/Code/Accumulator.cs | 7 + .../Core/Code/BlockCodeBuilder.cs | 26 ++- src/Pure.DI.Core/Core/Code/BuildContext.cs | 3 +- src/Pure.DI.Core/Core/Code/BuildTools.cs | 23 ++- .../Core/Code/CompositionBuilder.cs | 3 +- .../Core/Code/ConstructCodeBuilder.cs | 5 +- .../Core/Code/FactoryCodeBuilder.cs | 14 +- .../Core/Code/ImplementationCodeBuilder.cs | 6 +- .../Core/Code/VariablesBuilder.cs | 60 +++++- .../Core/DependencyGraphBuilder.cs | 62 +++++- src/Pure.DI.Core/Core/IMetadataVisitor.cs | 2 + src/Pure.DI.Core/Core/MetadataBuilder.cs | 3 + src/Pure.DI.Core/Core/MetadataWalkerBase.cs | 4 + .../Core/Models/DependencyNode.cs | 2 + src/Pure.DI.Core/Core/Models/MdAccumulator.cs | 10 + .../Core/Models/MdConstructKind.cs | 3 +- src/Pure.DI.Core/Core/Models/MdSetup.cs | 1 + src/Pure.DI.Core/Core/SetupsBuilder.cs | 12 +- src/Pure.DI.Core/Features/Default.g.cs | 3 + .../AccumulatorTests.cs | 176 ++++++++++++++++++ .../Pure.DI.IntegrationTests/FactoryTests.cs | 1 - .../ShroedingersCatTests.cs | 1 + .../TrackingDisposableInDelegatesScenario.cs | 82 ++++++++ .../Basics/TrackingDisposableScenario.cs | 77 ++++++++ ...ckingDisposableInstancesPerRootScenario.cs | 120 ------------ .../TrackingDisposableInstancesScenario.cs | 101 ---------- 28 files changed, 646 insertions(+), 248 deletions(-) create mode 100644 src/Pure.DI.Core/Core/Code/Accumulator.cs create mode 100644 src/Pure.DI.Core/Core/Models/MdAccumulator.cs create mode 100644 tests/Pure.DI.IntegrationTests/AccumulatorTests.cs create mode 100644 tests/Pure.DI.UsageTests/Basics/TrackingDisposableInDelegatesScenario.cs create mode 100644 tests/Pure.DI.UsageTests/Basics/TrackingDisposableScenario.cs delete mode 100644 tests/Pure.DI.UsageTests/Hints/TrackingDisposableInstancesPerRootScenario.cs delete mode 100644 tests/Pure.DI.UsageTests/Hints/TrackingDisposableInstancesScenario.cs diff --git a/src/Pure.DI.Core/Components/Api.g.cs b/src/Pure.DI.Core/Components/Api.g.cs index 3710daab9..55ed4122a 100644 --- a/src/Pure.DI.Core/Components/Api.g.cs +++ b/src/Pure.DI.Core/Components/Api.g.cs @@ -1087,6 +1087,70 @@ internal enum Tag /// Type } + + /// + /// Gives the opportunity to collect disposable objects. + /// + public class Owned : global::System.IDisposable + { + private bool _isDisposed; + private global::System.Collections.Generic.List _disposables + = new global::System.Collections.Generic.List(); + + /// + /// Adds a disposable instance. + /// + /// The disposable instance. + [global::System.Runtime.CompilerServices.MethodImpl((global::System.Runtime.CompilerServices.MethodImplOptions)256)] + public void Add(global::System.IDisposable disposable) + { + lock (_disposables) + { + _disposables.Add(disposable); + } + } + + /// + [global::System.Runtime.CompilerServices.MethodImpl((global::System.Runtime.CompilerServices.MethodImplOptions)512)] + public void Dispose() + { + global::System.Collections.Generic.List disposables; + lock (_disposables) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + disposables = _disposables; + } + + disposables.Reverse(); + global::System.Collections.Generic.List errors = null; + foreach (var disposable in disposables) + { + try + { + disposable.Dispose(); + } + catch (global::System.Exception error) + { + if (errors == null) + { + errors = new global::System.Collections.Generic.List(); + } + + errors.Add(error); + } + } + + if (errors != null) + { + throw new global::System.AggregateException(errors); + } + } + } /// /// An API for a Dependency Injection setup. @@ -1374,6 +1438,9 @@ internal interface IConfiguration /// Reference to the setup continuation chain. /// IConfiguration Hint(Hint hint, string value); + + IConfiguration Accumulate(Lifetime lifetime) + where TAccumulator: new(); } /// @@ -1929,6 +1996,14 @@ public IConfiguration Hint(Hint hint, string value) { return Configuration.Shared; } + + /// + [global::System.Runtime.CompilerServices.MethodImpl((global::System.Runtime.CompilerServices.MethodImplOptions)256)] + public IConfiguration Accumulate(Lifetime lifetime) + where TAccumulator: new() + { + return Configuration.Shared; + } } private sealed class Binding : IBinding diff --git a/src/Pure.DI.Core/Core/ApiInvocationProcessor.cs b/src/Pure.DI.Core/Core/ApiInvocationProcessor.cs index c0a66b84e..35fc46405 100644 --- a/src/Pure.DI.Core/Core/ApiInvocationProcessor.cs +++ b/src/Pure.DI.Core/Core/ApiInvocationProcessor.cs @@ -122,6 +122,7 @@ public void ProcessInvocation( ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Empty, + ImmutableArray.Empty, comments.FilterHints(invocationComments).ToList())); break; @@ -234,6 +235,17 @@ public void ProcessInvocation( metadataVisitor.VisitOrdinalAttribute(new MdOrdinalAttribute(semanticModel, invocation.ArgumentList, semanticModel.GetTypeSymbol(ordinalAttributeType, cancellationToken), BuildConstantArgs(semanticModel, invocation.ArgumentList.Arguments) is [int positionVal] ? positionVal : 0)); } + break; + + case nameof(IConfiguration.Accumulate): + if (genericName.TypeArgumentList.Arguments is [var typeSyntax, var accumulatorTypeSyntax] + && arguments.GetArgs(invocation.ArgumentList, "lifetime") is [{ Expression: {} lifetimeArgExpression }]) + { + var typeSymbol = semanticModel.GetTypeSymbol(typeSyntax, cancellationToken); + var accumulatorTypeSymbol = semanticModel.GetTypeSymbol(accumulatorTypeSyntax, cancellationToken); + metadataVisitor.VisitAccumulator(new MdAccumulator(semanticModel, invocation, typeSymbol, accumulatorTypeSymbol, semanticModel.GetConstantValue(lifetimeArgExpression))); + } + break; } diff --git a/src/Pure.DI.Core/Core/Code/Accumulator.cs b/src/Pure.DI.Core/Core/Code/Accumulator.cs new file mode 100644 index 000000000..36234f194 --- /dev/null +++ b/src/Pure.DI.Core/Core/Code/Accumulator.cs @@ -0,0 +1,7 @@ +namespace Pure.DI.Core.Code; + +internal record Accumulator( + string Name, + bool IsDeclared, + ITypeSymbol Type, + 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 df7da5f6d..27ba4a8a7 100644 --- a/src/Pure.DI.Core/Core/Code/BlockCodeBuilder.cs +++ b/src/Pure.DI.Core/Core/Code/BlockCodeBuilder.cs @@ -37,6 +37,23 @@ 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) + .Select(i => i.First()); + + foreach (var accumulator in uniqueAccumulators) + { + code.AppendLine($"var {accumulator.Name} = new {accumulator.AccumulatorType}();"); + accumulators.Add(accumulator with { IsDeclared = true }); + } + + if (accumulators.Count > 0) + { + ctx = ctx with { Accumulators = accumulators.ToImmutableArray() }; + } + if (toCheckExistence) { var checkExpression = variable.InstanceType.IsValueType @@ -62,7 +79,14 @@ variable.Node.Lifetime is Lifetime.Singleton or Lifetime.Scoped foreach (var statement in block.Statements) { - ctx.StatementBuilder.Build(ctx with { Variable = statement.Current, Code = code }, statement); + if (block.Current != statement.Current) + { + ctx.StatementBuilder.Build(ctx with { Variable = statement.Current, Code = code }, statement); + } + else + { + ctx.StatementBuilder.Build(ctx with { Variable = statement.Current, Code = code }, statement); + } } if (variable.Node.Lifetime is Lifetime.Singleton) diff --git a/src/Pure.DI.Core/Core/Code/BuildContext.cs b/src/Pure.DI.Core/Core/Code/BuildContext.cs index c0b6555ba..24e66bd70 100644 --- a/src/Pure.DI.Core/Core/Code/BuildContext.cs +++ b/src/Pure.DI.Core/Core/Code/BuildContext.cs @@ -9,4 +9,5 @@ internal record BuildContext( LinesBuilder Code, LinesBuilder LocalFunctionsCode, object? ContextTag, - bool? LockIsRequired); \ No newline at end of file + bool? LockIsRequired, + ImmutableArray Accumulators); \ No newline at end of file diff --git a/src/Pure.DI.Core/Core/Code/BuildTools.cs b/src/Pure.DI.Core/Core/Code/BuildTools.cs index d9e8a1a13..2d30f38e8 100644 --- a/src/Pure.DI.Core/Core/Code/BuildTools.cs +++ b/src/Pure.DI.Core/Core/Code/BuildTools.cs @@ -2,7 +2,11 @@ // ReSharper disable ClassNeverInstantiated.Global namespace Pure.DI.Core.Code; -internal class BuildTools(IFilter filter, ITypeResolver typeResolver) : IBuildTools +internal class BuildTools( + IFilter filter, + ITypeResolver typeResolver, + IBaseSymbolsProvider baseSymbolsProvider) + : IBuildTools { public void AddPureHeader(LinesBuilder code) { @@ -42,9 +46,19 @@ public IEnumerable OnCreated(BuildContext ctx, Variable variable) return Array.Empty(); } + var baseTypes = + baseSymbolsProvider.GetBaseSymbols(variable.InstanceType) + .Concat(Enumerable.Repeat(variable.InstanceType, 1)) + .ToImmutableHashSet(SymbolEqualityComparer.Default); + + var lines = ctx.Accumulators + .Where(i => baseTypes.Contains(i.Type)) + .Select(i => new Line(0, $"{i.Name}.Add({variable.VariableName});")) + .ToList(); + if (!ctx.DependencyGraph.Source.Hints.IsOnNewInstanceEnabled) { - return Array.Empty(); + return lines; } if (!filter.IsMeetRegularExpression( @@ -53,11 +67,12 @@ public IEnumerable OnCreated(BuildContext ctx, Variable variable) (Hint.OnNewInstanceTagRegularExpression, variable.Injection.Tag.ValueToString()), (Hint.OnNewInstanceLifetimeRegularExpression, variable.Node.Lifetime.ValueToString()))) { - return Array.Empty(); + return lines; } var tag = GetTag(ctx, variable); - return [new Line(0, $"{Names.OnNewInstanceMethodName}<{typeResolver.Resolve(variable.InstanceType)}>(ref {variable.VariableName}, {tag.ValueToString()}, {variable.Node.Lifetime.ValueToString()})" + ";")]; + lines.Insert(0, new Line(0, $"{Names.OnNewInstanceMethodName}<{typeResolver.Resolve(variable.InstanceType)}>(ref {variable.VariableName}, {tag.ValueToString()}, {variable.Node.Lifetime.ValueToString()});")); + return lines; } private static object? GetTag(BuildContext ctx, Variable variable) diff --git a/src/Pure.DI.Core/Core/Code/CompositionBuilder.cs b/src/Pure.DI.Core/Core/Code/CompositionBuilder.cs index 3362c780c..7dee7b69a 100644 --- a/src/Pure.DI.Core/Core/Code/CompositionBuilder.cs +++ b/src/Pure.DI.Core/Core/Code/CompositionBuilder.cs @@ -31,7 +31,8 @@ public CompositionCode Build(DependencyGraph graph) new LinesBuilder(), new LinesBuilder(), root.Injection.Tag != MdTag.ContextTag ? root.Injection.Tag : default, - default); + default, + root.Node.Accumulators.ToImmutableArray()); foreach (var perResolveVar in map.GetPerResolves()) { diff --git a/src/Pure.DI.Core/Core/Code/ConstructCodeBuilder.cs b/src/Pure.DI.Core/Core/Code/ConstructCodeBuilder.cs index be75d7fc0..41e5fe417 100644 --- a/src/Pure.DI.Core/Core/Code/ConstructCodeBuilder.cs +++ b/src/Pure.DI.Core/Core/Code/ConstructCodeBuilder.cs @@ -35,12 +35,15 @@ public void Build(BuildContext ctx, in DpConstruct construct) case MdConstructKind.AsyncEnumerable: BuildEnumerable(ctx, construct,"async "); break; + + case MdConstructKind.Accumulator: + break; default: throw new ArgumentOutOfRangeException(); } } - + private void BuildEnumerable(BuildContext ctx, in DpConstruct enumerable, string methodPrefix = "") { var variable = ctx.Variable; diff --git a/src/Pure.DI.Core/Core/Code/FactoryCodeBuilder.cs b/src/Pure.DI.Core/Core/Code/FactoryCodeBuilder.cs index 3d58de0ab..f10cb9966 100644 --- a/src/Pure.DI.Core/Core/Code/FactoryCodeBuilder.cs +++ b/src/Pure.DI.Core/Core/Code/FactoryCodeBuilder.cs @@ -1,5 +1,6 @@ // ReSharper disable ClassNeverInstantiated.Global // ReSharper disable ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator +// ReSharper disable MergeIntoPattern namespace Pure.DI.Core.Code; internal class FactoryCodeBuilder( @@ -54,6 +55,17 @@ public void Build(BuildContext ctx, in DpFactory factory) using var resolvers = injections .Zip(variable.Args, (injection, argument) => (injection, argument)) .GetEnumerator(); + + var injectionsCtx = ctx; + if (variable.IsLazy && variable.Node.Accumulators.Count > 0) + { + injectionsCtx = injectionsCtx with + { + Accumulators = injectionsCtx.Accumulators.AddRange( + variable.Node.Accumulators + .Select(accumulator => accumulator with { IsDeclared = false })) + }; + } var indent = new Indent(0); var text = syntaxNode.GetText(); @@ -66,7 +78,7 @@ public void Build(BuildContext ctx, in DpFactory factory) var (injection, argument) = resolvers.Current; using (code.Indent(indent.Value)) { - ctx.StatementBuilder.Build(ctx with { Level = level, Variable = argument.Current, LockIsRequired = lockIsRequired }, argument); + ctx.StatementBuilder.Build(injectionsCtx with { Level = level, Variable = argument.Current, LockIsRequired = lockIsRequired }, argument); code.AppendLine($"{(injection.DeclarationRequired ? "var " : "")}{injection.VariableName} = {ctx.BuildTools.OnInjected(ctx, argument.Current)};"); } } diff --git a/src/Pure.DI.Core/Core/Code/ImplementationCodeBuilder.cs b/src/Pure.DI.Core/Core/Code/ImplementationCodeBuilder.cs index eef703fde..f7e4a9113 100644 --- a/src/Pure.DI.Core/Core/Code/ImplementationCodeBuilder.cs +++ b/src/Pure.DI.Core/Core/Code/ImplementationCodeBuilder.cs @@ -59,12 +59,12 @@ public void Build(BuildContext ctx, in DpImplementation implementation) } var onCreatedStatements = ctx.BuildTools.OnCreated(ctx, ctx.Variable).ToArray(); - var hasOnCreatedHandler = ctx.BuildTools.OnCreated(ctx, variable).Any(); + var hasOnCreatedStatements = ctx.BuildTools.OnCreated(ctx, variable).Any(); var hasAlternativeInjections = visits.Any(); var tempVariableInit = ctx.DependencyGraph.Source.Hints.IsThreadSafeEnabled && ctx.Variable.Node.Lifetime is not Lifetime.Transient and not Lifetime.PerBlock - && (hasAlternativeInjections || hasOnCreatedHandler); + && (hasAlternativeInjections || hasOnCreatedStatements); if (tempVariableInit) { @@ -80,7 +80,7 @@ public void Build(BuildContext ctx, in DpImplementation implementation) if (variable.Node.Lifetime is not Lifetime.Transient || hasAlternativeInjections || tempVariableInit - || hasOnCreatedHandler) + || hasOnCreatedStatements) { ctx.Code.Append($"{ctx.BuildTools.GetDeclaration(variable)}{ctx.Variable.VariableName} = "); ctx.Code.Append(instantiation); diff --git a/src/Pure.DI.Core/Core/Code/VariablesBuilder.cs b/src/Pure.DI.Core/Core/Code/VariablesBuilder.cs index 4d3c679be..be4b02fd1 100644 --- a/src/Pure.DI.Core/Core/Code/VariablesBuilder.cs +++ b/src/Pure.DI.Core/Core/Code/VariablesBuilder.cs @@ -36,19 +36,42 @@ public Block Build( case Variable variable: { - if (!graph.TryGetInEdges(variable.Node, out var dependencies) - || dependencies.Count == 0) + var isAccumulator = IsAccumulator(variable, out var construct); + IReadOnlyCollection dependencies = Array.Empty(); + if (!isAccumulator) { - continue; + if (!graph.TryGetInEdges(variable.Node, out dependencies) + || dependencies.Count == 0) + { + continue; + } } var pathIds = new HashSet(); var hasLazy = false; + ICollection? accumulators = default; foreach (var pathItem in currentStatement.GetPath()) { var pathVar = pathItem.Current; pathIds.Add(pathVar.Node.Binding.Id); - hasLazy |= pathVar.IsLazy; + if (!pathVar.IsLazy) + { + continue; + } + + hasLazy = true; + if (accumulators != default) + { + continue; + } + + accumulators = pathVar.Node.Accumulators; + } + + accumulators ??= rootNode.Accumulators; + if (isAccumulator) + { + accumulators.Add(new Accumulator(GetAccumulatorName(variable), false, construct.ElementType, construct.Type)); } foreach (var (isDepResolved, depNode, depInjection, _) in dependencies) @@ -113,7 +136,22 @@ public Block Build( return rootBlock; } - + + private static bool IsAccumulator(Variable variable, out MdConstruct mdConstruct) + { + if(variable.Node.Construct?.Source is { Kind: MdConstructKind.Accumulator } construct) + { + mdConstruct = construct; + return true; + } + + mdConstruct = default; + return false; + } + + private static string GetAccumulatorName(Variable variable) => + $"accumulator{Names.Salt}{variable.Node.Binding.Id}"; + private static Variable GetVariable( Block parentBlock, IDictionary map, @@ -129,8 +167,16 @@ private static Variable GetVariable( switch (node.Lifetime) { case Lifetime.Transient: - return new Variable(parentBlock, transientId++, node, injection, new List(), new VariableInfo(), node.IsLazy(), hasCycle); - + { + var transientVariable = new Variable(parentBlock, transientId++, node, injection, new List(), new VariableInfo(), node.IsLazy(), hasCycle); + if (node.Construct?.Source.Kind == MdConstructKind.Accumulator) + { + transientVariable.VariableCode = GetAccumulatorName(transientVariable); + } + + return transientVariable; + } + case Lifetime.PerBlock: { var perBlockKey = (node.Binding, parentBlock.Id); diff --git a/src/Pure.DI.Core/Core/DependencyGraphBuilder.cs b/src/Pure.DI.Core/Core/DependencyGraphBuilder.cs index ac092e72f..3d4d1d143 100644 --- a/src/Pure.DI.Core/Core/DependencyGraphBuilder.cs +++ b/src/Pure.DI.Core/Core/DependencyGraphBuilder.cs @@ -51,6 +51,12 @@ public IEnumerable TryBuild( } } + var accumulators = new Dictionary(); + foreach (var accumulator in setup.Accumulators) + { + accumulators[new AccumulatorKey(accumulator.AccumulatorType, accumulator.Lifetime)] = accumulator; + } + var processed = new HashSet(); var notProcessed = new HashSet(); var edgesMap = new Dictionary>(); @@ -69,6 +75,32 @@ public IEnumerable TryBuild( } } + if (accumulators.TryGetValue(new AccumulatorKey(injection.Type, node.Node.Lifetime), out var accumulator)) + { + var accumulatorBinding = new MdBinding( + ++maxId, + targetNode.Binding.Source, + setup, + targetNode.Binding.SemanticModel, + ImmutableArray.Create(new MdContract(targetNode.Binding.SemanticModel, accumulator.Source, accumulator.AccumulatorType, ImmutableArray.Empty)), + ImmutableArray.Empty, + new MdLifetime(targetNode.Binding.SemanticModel, accumulator.Source, Lifetime.Transient), + default, + default, + default, + new MdConstruct( + targetNode.Binding.SemanticModel, + targetNode.Binding.Source, + accumulator.AccumulatorType, + accumulator.Type, + MdConstructKind.Accumulator, + ImmutableArray.Empty, + hasExplicitDefaultValue, + explicitDefaultValue)); + + return CreateNodes(setup, accumulatorBinding); + } + switch (injection.Type) { case INamedTypeSymbol { IsGenericType: true } geneticType: @@ -122,8 +154,8 @@ public IEnumerable TryBuild( var lifetime = constructKind == MdConstructKind.Enumerable ? Lifetime.PerBlock : Lifetime.Transient; if (constructKind.HasValue) { - var enumerableBinding = CreateConstructBinding(setup, targetNode, injection, constructType, default, lifetime, ++maxId, constructKind.Value, false, default); - return CreateNodes(setup, enumerableBinding); + var constructBinding = CreateConstructBinding(setup, targetNode, injection, constructType, default, lifetime, ++maxId, constructKind.Value, false, default); + return CreateNodes(setup, constructBinding); } } @@ -479,4 +511,30 @@ private IEnumerable CreateNodes(MdSetup setup, MdBinding binding var newSetup = setup with { Roots = ImmutableArray.Empty, Bindings = ImmutableArray.Create(binding) }; return dependencyNodeBuilders.SelectMany(builder => builder.Build(newSetup)); } + + private readonly struct AccumulatorKey + { + private readonly ITypeSymbol _type; + private readonly Lifetime _lifetime; + + public AccumulatorKey(ITypeSymbol type, Lifetime lifetime) + { + _type = type; + _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/IMetadataVisitor.cs b/src/Pure.DI.Core/Core/IMetadataVisitor.cs index d514bb78a..b2f0be325 100644 --- a/src/Pure.DI.Core/Core/IMetadataVisitor.cs +++ b/src/Pure.DI.Core/Core/IMetadataVisitor.cs @@ -35,5 +35,7 @@ internal interface IMetadataVisitor void VisitTag(in MdTag tag); + void VisitAccumulator(in MdAccumulator accumulator); + void VisitFinish(); } \ No newline at end of file diff --git a/src/Pure.DI.Core/Core/MetadataBuilder.cs b/src/Pure.DI.Core/Core/MetadataBuilder.cs index 2647aa7a1..f748e1cf5 100644 --- a/src/Pure.DI.Core/Core/MetadataBuilder.cs +++ b/src/Pure.DI.Core/Core/MetadataBuilder.cs @@ -107,6 +107,7 @@ private void MergeSetups(IEnumerable setups, out MdSetup mergedSetup, b var tagAttributesBuilder = ImmutableArray.CreateBuilder(2); var ordinalAttributesBuilder = ImmutableArray.CreateBuilder(2); var usingDirectives = ImmutableArray.CreateBuilder(2); + var accumulators = ImmutableArray.CreateBuilder(1); var bindingId = 0; var comments = new List(); foreach (var setup in setups) @@ -133,6 +134,7 @@ private void MergeSetups(IEnumerable setups, out MdSetup mergedSetup, b typeAttributesBuilder.AddRange(setup.TypeAttributes); tagAttributesBuilder.AddRange(setup.TagAttributes); ordinalAttributesBuilder.AddRange(setup.OrdinalAttributes); + accumulators.AddRange(setup.Accumulators); foreach (var usingDirective in setup.UsingDirectives) { usingDirectives.Add(usingDirective); @@ -158,6 +160,7 @@ private void MergeSetups(IEnumerable setups, out MdSetup mergedSetup, b typeAttributesBuilder.ToImmutable(), tagAttributesBuilder.ToImmutable(), ordinalAttributesBuilder.ToImmutable(), + accumulators.ToImmutable(), comments); } } \ No newline at end of file diff --git a/src/Pure.DI.Core/Core/MetadataWalkerBase.cs b/src/Pure.DI.Core/Core/MetadataWalkerBase.cs index 9835807f1..95096840a 100644 --- a/src/Pure.DI.Core/Core/MetadataWalkerBase.cs +++ b/src/Pure.DI.Core/Core/MetadataWalkerBase.cs @@ -149,6 +149,10 @@ public virtual void VisitLifetime(in MdLifetime lifetime) public virtual void VisitTag(in MdTag tag) { } + + public virtual void VisitAccumulator(in MdAccumulator accumulator) + { + } public virtual void VisitFinish() { diff --git a/src/Pure.DI.Core/Core/Models/DependencyNode.cs b/src/Pure.DI.Core/Core/Models/DependencyNode.cs index 582e0a4ac..c151a13f5 100644 --- a/src/Pure.DI.Core/Core/Models/DependencyNode.cs +++ b/src/Pure.DI.Core/Core/Models/DependencyNode.cs @@ -4,6 +4,7 @@ internal record DependencyNode( int Variation, in MdBinding Binding, ITypeSymbol Type, + ICollection Accumulators, in DpRoot? Root = default, in DpImplementation? Implementation = default, in DpFactory? Factory = default, @@ -22,6 +23,7 @@ public DependencyNode( Variation, binding, Root?.Source.RootType ?? Implementation?.Source.Type ?? Factory?.Source.Type ?? Arg?.Source.Type ?? Construct?.Source.Type!, + new List(), Root, Implementation, Factory, diff --git a/src/Pure.DI.Core/Core/Models/MdAccumulator.cs b/src/Pure.DI.Core/Core/Models/MdAccumulator.cs new file mode 100644 index 000000000..4774ef220 --- /dev/null +++ b/src/Pure.DI.Core/Core/Models/MdAccumulator.cs @@ -0,0 +1,10 @@ +// ReSharper disable HeapView.ObjectAllocation +// ReSharper disable NotAccessedPositionalProperty.Global +namespace Pure.DI.Core.Models; + +internal readonly record struct MdAccumulator( + SemanticModel SemanticModel, + SyntaxNode Source, + ITypeSymbol Type, + ITypeSymbol AccumulatorType, + Lifetime Lifetime); \ No newline at end of file diff --git a/src/Pure.DI.Core/Core/Models/MdConstructKind.cs b/src/Pure.DI.Core/Core/Models/MdConstructKind.cs index 79a6c27a2..53bf52df3 100644 --- a/src/Pure.DI.Core/Core/Models/MdConstructKind.cs +++ b/src/Pure.DI.Core/Core/Models/MdConstructKind.cs @@ -9,5 +9,6 @@ internal enum MdConstructKind Composition, OnCannotResolve, ExplicitDefaultValue, - AsyncEnumerable + AsyncEnumerable, + Accumulator } \ No newline at end of file diff --git a/src/Pure.DI.Core/Core/Models/MdSetup.cs b/src/Pure.DI.Core/Core/Models/MdSetup.cs index 3ce79b03c..929ad736c 100644 --- a/src/Pure.DI.Core/Core/Models/MdSetup.cs +++ b/src/Pure.DI.Core/Core/Models/MdSetup.cs @@ -15,5 +15,6 @@ internal record MdSetup( in ImmutableArray TypeAttributes, in ImmutableArray TagAttributes, in ImmutableArray OrdinalAttributes, + in ImmutableArray Accumulators, IReadOnlyCollection Comments, ITypeConstructor? TypeConstructor = default); \ No newline at end of file diff --git a/src/Pure.DI.Core/Core/SetupsBuilder.cs b/src/Pure.DI.Core/Core/SetupsBuilder.cs index 870e88e60..81baaeaf0 100644 --- a/src/Pure.DI.Core/Core/SetupsBuilder.cs +++ b/src/Pure.DI.Core/Core/SetupsBuilder.cs @@ -15,6 +15,7 @@ internal sealed class SetupsBuilder( private readonly List _tagAttributes = []; private readonly List _ordinalAttributes = []; private readonly List _usingDirectives = []; + private readonly List _accumulators = []; private IBindingBuilder _bindingBuilder = bindingBuilderFactory(); private MdSetup? _setup; @@ -94,12 +95,13 @@ public void VisitLifetime(in MdLifetime lifetime) => public void VisitTag(in MdTag tag) => _bindingBuilder.AddTag(tag); + public void VisitAccumulator(in MdAccumulator accumulator) => + _accumulators.Add(accumulator); + public void VisitFinish() => FinishSetup(); - private void FinishBinding() - { + private void FinishBinding() => _bindings.Add(_bindingBuilder.Build(_setup!)); - } private void FinishSetup() { @@ -117,7 +119,8 @@ setup with TypeAttributes = _typeAttributes.ToImmutableArray(), TagAttributes = _tagAttributes.ToImmutableArray(), OrdinalAttributes = _ordinalAttributes.ToImmutableArray(), - UsingDirectives = _usingDirectives.ToImmutableArray() + UsingDirectives = _usingDirectives.ToImmutableArray(), + Accumulators = _accumulators.ToImmutableArray() }); _bindings.Clear(); @@ -126,6 +129,7 @@ setup with _typeAttributes.Clear(); _ordinalAttributes.Clear(); _usingDirectives.Clear(); + _accumulators.Clear(); _setup = default; _bindingBuilder = bindingBuilderFactory(); } diff --git a/src/Pure.DI.Core/Features/Default.g.cs b/src/Pure.DI.Core/Features/Default.g.cs index 91bf63c32..e9b0e8233 100644 --- a/src/Pure.DI.Core/Features/Default.g.cs +++ b/src/Pure.DI.Core/Features/Default.g.cs @@ -13,6 +13,9 @@ private static void Setup() .TypeAttribute() .TagAttribute() .OrdinalAttribute() + .Accumulate(Lifetime.Transient) + .Accumulate(Lifetime.PerResolve) + .Accumulate(Lifetime.PerBlock) .Bind>() .As(Lifetime.PerResolve) .To(ctx => new global::System.Func(() => diff --git a/tests/Pure.DI.IntegrationTests/AccumulatorTests.cs b/tests/Pure.DI.IntegrationTests/AccumulatorTests.cs new file mode 100644 index 000000000..5dfa6f2f2 --- /dev/null +++ b/tests/Pure.DI.IntegrationTests/AccumulatorTests.cs @@ -0,0 +1,176 @@ +namespace Pure.DI.IntegrationTests; + +public class AccumulatorTests +{ + [Fact] + public async Task ShouldSupportAccumulator() + { + // 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 DependencyAccumulator: List + { + } + + static class Setup + { + private static void SetupComposition() + { + DI.Setup("Composition") + .Bind().To(ctx => new Dependency()) + .Bind().To() + .Accumulate(Lifetime.Transient) + .Root<(IService service, DependencyAccumulator dependencies)>("Service"); + } + } + + public class Program + { + public static void Main() + { + var composition = new Composition(); + var root = composition.Service; + var service = root.service; + } + } +} +""".RunAsync(); + + // Then + result.Success.ShouldBeTrue(result); + } + + [Fact] + public async Task ShroedingersCatScenarioWhenAccumulator() + { + // Given + + // When + var result = await """ +using System; +using System.Collections.Generic; +using Pure.DI; +using static Pure.DI.Lifetime; + +namespace Sample +{ + // Let's create an abstraction + + interface IBox { T Content { get; } } + + enum State { Alive, Dead } + + interface ICat { State State { get; } } + + // Here is our implementation + + class CardboardBox : IBox + { + public CardboardBox(Func<(T value, Accumulator acc)> contentFactory) + { + var content = contentFactory(); + foreach(var dep in content.acc) + { + Console.WriteLine(dep); + } + + Content = content.value; + } + + public T Content { get; } + + public override string ToString() => $"[{Content}]"; + } + + class ShroedingersCat : ICat + { + // Represents the superposition of the states + private readonly Lazy _superposition; + + public ShroedingersCat(Lazy superposition) => _superposition = superposition; + + // The decoherence of the superposition at the time of observation via an irreversible process + public State State => _superposition.Value; + + public override string ToString() => $"{State} cat"; + } + + class Accumulator: List { } + + // Let's glue all together + + internal partial class Composition + { + private static void Setup() + { + // FormatCode = On + DI.Setup(nameof(Composition)) + .Accumulate(Transient) + .Accumulate(Singleton) + // Models a random subatomic event that may or may not occur + .Bind().As(Singleton).To() + // Represents a quantum superposition of 2 states: Alive or Dead + .Bind().To(ctx => + { + ctx.Inject(out var random); + return (State)random.Next(2); + }) + .Bind().To() + // Represents a cardboard box with any content + .Bind>().To>() + // Composition Root + .Root<(Program program, Accumulator)>("Root"); + } + } + + public class Program + { + IBox _box; + + internal Program(IBox box) => _box = box; + + public static void Main() + { + var composition = new Composition(); + var root = composition.Root; + } + } +} +""".RunAsync(new Options + { + LanguageVersion = LanguageVersion.CSharp8, + NullableContextOptions = NullableContextOptions.Disable, + PreprocessorSymbols = ["NET", "NET6_0_OR_GREATER"] + } ); + + // Then + result.Success.ShouldBeTrue(result); + result.StdOut.Length.ShouldBe(3); + } +} \ No newline at end of file diff --git a/tests/Pure.DI.IntegrationTests/FactoryTests.cs b/tests/Pure.DI.IntegrationTests/FactoryTests.cs index 9188d7a4c..3e24e2b97 100644 --- a/tests/Pure.DI.IntegrationTests/FactoryTests.cs +++ b/tests/Pure.DI.IntegrationTests/FactoryTests.cs @@ -1159,5 +1159,4 @@ public static void Main() result.Success.ShouldBeTrue(result); result.GeneratedCode.Split(Environment.NewLine).Count(i => i.Contains(" = new Sample.Dependency2();")).ShouldBe(2); } - } \ No newline at end of file diff --git a/tests/Pure.DI.IntegrationTests/ShroedingersCatTests.cs b/tests/Pure.DI.IntegrationTests/ShroedingersCatTests.cs index 5f95add5b..9c830cd40 100644 --- a/tests/Pure.DI.IntegrationTests/ShroedingersCatTests.cs +++ b/tests/Pure.DI.IntegrationTests/ShroedingersCatTests.cs @@ -53,6 +53,7 @@ internal partial class Composition { private static void Setup() { + // FormatCode = On DI.Setup(nameof(Composition)) // Models a random subatomic event that may or may not occur .Bind().As(Singleton).To() diff --git a/tests/Pure.DI.UsageTests/Basics/TrackingDisposableInDelegatesScenario.cs b/tests/Pure.DI.UsageTests/Basics/TrackingDisposableInDelegatesScenario.cs new file mode 100644 index 000000000..e9220e16f --- /dev/null +++ b/tests/Pure.DI.UsageTests/Basics/TrackingDisposableInDelegatesScenario.cs @@ -0,0 +1,82 @@ +/* +$v=true +$p=16 +$d=Tracking disposable instances in delegates +*/ + +// ReSharper disable CheckNamespace +// ReSharper disable UnusedMember.Local +// ReSharper disable UnusedParameterInPartialMethod +// ReSharper disable ArrangeTypeModifiers +namespace Pure.DI.UsageTests.Basics.TrackingDisposableInDelegatesScenario; + +using Xunit; + +// { +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(Func<(IDependency dependency, Owned owned)> dependencyFactory) + : IService, IDisposable +{ + private readonly (IDependency value, Owned owned) _dependency = dependencyFactory(); + + public IDependency Dependency => _dependency.value; + + public void Dispose() => _dependency.owned.Dispose(); +} + +partial class Composition +{ + private void Setup() => + DI.Setup(nameof(Composition)) + .Bind().To() + .Bind().To() + .Root("Root"); +} +// } + +public class Scenario +{ + [Fact] + public void Run() + { +// { + var composition = new Composition(); + + var root1 = composition.Root; + var root2 = composition.Root; + + root2.Dispose(); + + // Checks that the disposable instances + // associated with root1 have been disposed of + root2.Dependency.IsDisposed.ShouldBeTrue(); + + // Checks that the disposable instances + // associated with root2 have not been disposed of + root1.Dependency.IsDisposed.ShouldBeFalse(); + + root1.Dispose(); + + // Checks that the disposable instances + // associated with root2 have been disposed of + root1.Dependency.IsDisposed.ShouldBeTrue(); + // } + new Composition().SaveClassDiagram(); + } +} \ No newline at end of file diff --git a/tests/Pure.DI.UsageTests/Basics/TrackingDisposableScenario.cs b/tests/Pure.DI.UsageTests/Basics/TrackingDisposableScenario.cs new file mode 100644 index 000000000..b2d88d52a --- /dev/null +++ b/tests/Pure.DI.UsageTests/Basics/TrackingDisposableScenario.cs @@ -0,0 +1,77 @@ +/* +$v=true +$p=16 +$d=Tracking disposable instances per a composition root +*/ + +// ReSharper disable CheckNamespace +// ReSharper disable UnusedMember.Local +// ReSharper disable UnusedParameterInPartialMethod +// ReSharper disable ArrangeTypeModifiers +namespace Pure.DI.UsageTests.Basics.TrackingDisposableScenario; + +using Xunit; + +// { +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(IDependency dependency) : IService +{ + public IDependency Dependency { get; } = dependency; +} + +partial class Composition +{ + private void Setup() => + DI.Setup(nameof(Composition)) + .Bind().To() + .Bind().To() + .Root<(IService service, Owned owned)>("Root"); +} +// } + +public class Scenario +{ + [Fact] + public void Run() + { +// { + var composition = new Composition(); + + var root1 = composition.Root; + var root2 = composition.Root; + + root2.owned.Dispose(); + + // Checks that the disposable instances + // associated with root1 have been disposed of + root2.service.Dependency.IsDisposed.ShouldBeTrue(); + + // Checks that the disposable instances + // associated with root2 have not been disposed of + root1.service.Dependency.IsDisposed.ShouldBeFalse(); + + root1.owned.Dispose(); + + // Checks that the disposable instances + // associated with root2 have been disposed of + root1.service.Dependency.IsDisposed.ShouldBeTrue(); + // } + new Composition().SaveClassDiagram(); + } +} \ No newline at end of file diff --git a/tests/Pure.DI.UsageTests/Hints/TrackingDisposableInstancesPerRootScenario.cs b/tests/Pure.DI.UsageTests/Hints/TrackingDisposableInstancesPerRootScenario.cs deleted file mode 100644 index 5f04d12d6..000000000 --- a/tests/Pure.DI.UsageTests/Hints/TrackingDisposableInstancesPerRootScenario.cs +++ /dev/null @@ -1,120 +0,0 @@ -/* -$v=true -$p=6 -$d=Tracking disposable instances per a composition root -*/ - -// ReSharper disable CheckNamespace -// ReSharper disable UnusedMember.Local -// ReSharper disable UnusedParameterInPartialMethod -// ReSharper disable ArrangeTypeModifiers -namespace Pure.DI.UsageTests.Hints.TrackingDisposableInstancesPerRootScenario; - -using System.Collections.Concurrent; -using Xunit; - -// { -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(IDependency dependency) : IService -{ - public IDependency Dependency { get; } = dependency; -} - -partial class Composition -{ - private ConcurrentDictionary> _disposables = []; - - private void Setup() => - DI.Setup(nameof(Composition)) - // Specifies to call the partial method OnNewInstance - // when an instance is created - .Hint(Hint.OnNewInstance, "On") - - .Bind().To() - .Bind().To() - .Root>("Root"); - - partial void OnNewInstance(ref T value, object? tag, Lifetime lifetime) - { - if (value is IOwned || value is not IDisposable disposable - || lifetime is Lifetime.Singleton or Lifetime.Scoped) - { - return; - } - - _disposables.GetOrAdd(Environment.CurrentManagedThreadId, _ => []) - .Add(disposable); - } - - public interface IOwned; - - public readonly struct Owned: IDisposable, IOwned - { - public readonly T Value; - private readonly List _disposable; - - public Owned(T value, Composition composition) - { - Value = value; - _disposable = composition._disposables.TryRemove(Environment.CurrentManagedThreadId, out var disposables) - ? disposables - : []; - - composition._disposables = []; - } - - public void Dispose() - { - _disposable.Reverse(); - _disposable.ForEach(i => i.Dispose()); - } - } -} -// } - -public class Scenario -{ - [Fact] - public void Run() - { -// { - var composition = new Composition(); - - var root1 = composition.Root; - var root2 = composition.Root; - - root2.Dispose(); - - // Checks that the disposable instances - // associated with root1 have been disposed of - root2.Value.Dependency.IsDisposed.ShouldBeTrue(); - - // Checks that the disposable instances - // associated with root2 have not been disposed of - root1.Value.Dependency.IsDisposed.ShouldBeFalse(); - - root1.Dispose(); - - // Checks that the disposable instances - // associated with root2 have been disposed of - root1.Value.Dependency.IsDisposed.ShouldBeTrue(); - // } - new Composition().SaveClassDiagram(); - } -} \ No newline at end of file diff --git a/tests/Pure.DI.UsageTests/Hints/TrackingDisposableInstancesScenario.cs b/tests/Pure.DI.UsageTests/Hints/TrackingDisposableInstancesScenario.cs deleted file mode 100644 index e716660f1..000000000 --- a/tests/Pure.DI.UsageTests/Hints/TrackingDisposableInstancesScenario.cs +++ /dev/null @@ -1,101 +0,0 @@ -/* -$v=true -$p=6 -$d=Tracking disposable instances -*/ - -// ReSharper disable CheckNamespace -// ReSharper disable UnusedMember.Local -// ReSharper disable UnusedParameterInPartialMethod -// ReSharper disable ArrangeTypeModifiers -namespace Pure.DI.UsageTests.Hints.TrackingDisposableInstancesScenario; - -using Xunit; - -// { -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(IDependency dependency) : IService -{ - public IDependency Dependency { get; } = dependency; -} - -partial class Composition -{ - public event Action OnNewDisposable; - - private static void Setup() => - DI.Setup(nameof(Composition)) - // Specifies to call a partial method - // named OnNewInstance when an instance is created - .Hint(Hint.OnNewInstance, "On") - - // Specifies to call the partial method - // only for instances with lifetime - // Transient, PerResolve and PerBlock - .Hint( - Hint.OnNewInstanceLifetimeRegularExpression, - "Transient|PerResolve|PerBlock") - - .Bind().To() - .Bind().To() - .Root("Root"); - - partial void OnNewInstance( - ref T value, - object? tag, - Lifetime lifetime) - { - if (value is IDisposable disposable - && OnNewDisposable is {} onNewDisposable) - { - onNewDisposable(disposable); - } - } -} -// } - -public class Scenario -{ - [Fact] - public void Run() - { -// { - var composition = new Composition(); - - // Tracking disposable instances within a composition - var disposables = new Stack(); - composition.OnNewDisposable += disposable => - disposables.Push(disposable); - - var service = composition.Root; - disposables.Count.ShouldBe(1); - - // Disposal of instances in reverse order - while (disposables.TryPop(out var disposable)) - { - disposable.Dispose(); - } - - // Verifies that the disposable instance - // has been disposed of - service.Dependency.IsDisposed.ShouldBeTrue(); - // } - new Composition().SaveClassDiagram(); - } -} \ No newline at end of file