From 923b6d62b85471ce536c8c662289054c092aebcb Mon Sep 17 00:00:00 2001 From: Peter Csajtai Date: Fri, 15 Sep 2023 21:05:19 +0200 Subject: [PATCH] Add auto lifetime --- src/Lifetime/AutoLifetime.cs | 42 ++++++++++++++ src/Lifetime/EmptyLifetime.cs | 5 ++ src/Lifetime/ExpressionLifetimeDescriptor.cs | 12 +++- src/Lifetime/FactoryLifetimeDescriptor.cs | 13 ++++- src/Lifetime/LifetimeDescriptor.cs | 13 ++++- src/Lifetime/Lifetimes.cs | 10 ++++ src/Lifetime/NamedScopeLifetime.cs | 4 +- src/Lifetime/PerRequestLifetime.cs | 4 +- src/Lifetime/PerScopedRequestLifetime.cs | 2 +- src/Lifetime/ScopedLifetime.cs | 6 +- src/Lifetime/SingletonLifetime.cs | 6 +- src/Lifetime/TransientLifetime.cs | 2 +- .../Fluent/BaseFluentConfigurator.cs | 8 +++ src/Resolution/ResolutionContext.cs | 22 +++++++- test/LifetimeTests.cs | 55 +++++++++++++++++++ 15 files changed, 181 insertions(+), 23 deletions(-) create mode 100644 src/Lifetime/AutoLifetime.cs diff --git a/src/Lifetime/AutoLifetime.cs b/src/Lifetime/AutoLifetime.cs new file mode 100644 index 00000000..b9f8267b --- /dev/null +++ b/src/Lifetime/AutoLifetime.cs @@ -0,0 +1,42 @@ +using System.Linq.Expressions; +using Stashbox.Registration; +using Stashbox.Resolution; + +namespace Stashbox.Lifetime; + +internal class AutoLifetime : LifetimeDescriptor +{ + private LifetimeDescriptor selectedLifetime; + + internal override bool StoreResultInLocalVariable => this.selectedLifetime.StoreResultInLocalVariable; + + protected internal override int LifeSpan => this.selectedLifetime.LifeSpan; + + public AutoLifetime(LifetimeDescriptor boundaryLifetime) + { + this.selectedLifetime = boundaryLifetime; + } + + private protected override Expression? BuildLifetimeAppliedExpression(ServiceRegistration serviceRegistration, + ResolutionContext resolutionContext, TypeInformation typeInformation) + { + var tracker = new ResolutionContext.AutoLifetimeTracker(); + var context = resolutionContext.BeginAutoLifetimeTrackingContext(tracker); + var expression = GetExpressionForRegistration(serviceRegistration, context, typeInformation); + this.selectedLifetime = tracker.HighestRankingLifetime.LifeSpan <= this.selectedLifetime.LifeSpan + ? tracker.HighestRankingLifetime + : this.selectedLifetime; + + var func = expression?.CompileDelegate(context, context.CurrentContainerContext.ContainerConfiguration); + if (func == null) return null; + + var final = Expression.Invoke(func.AsConstant(), context.CurrentScopeParameter, context.RequestContextParameter); + + return this.selectedLifetime.ApplyLifetimeToExpression(final, serviceRegistration, resolutionContext, typeInformation); + } + + internal override Expression? ApplyLifetimeToExpression(Expression? expression, + ServiceRegistration serviceRegistration, + ResolutionContext resolutionContext, TypeInformation typeInformation) => + this.selectedLifetime.ApplyLifetimeToExpression(expression, serviceRegistration, resolutionContext, typeInformation); +} \ No newline at end of file diff --git a/src/Lifetime/EmptyLifetime.cs b/src/Lifetime/EmptyLifetime.cs index 3d481c60..b52b515f 100644 --- a/src/Lifetime/EmptyLifetime.cs +++ b/src/Lifetime/EmptyLifetime.cs @@ -9,4 +9,9 @@ internal class EmptyLifetime : LifetimeDescriptor private protected override Expression? BuildLifetimeAppliedExpression(ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, TypeInformation typeInformation) => null; + + internal override Expression? ApplyLifetimeToExpression(Expression? expression, + ServiceRegistration serviceRegistration, + ResolutionContext resolutionContext, TypeInformation typeInformation) + => null; } \ No newline at end of file diff --git a/src/Lifetime/ExpressionLifetimeDescriptor.cs b/src/Lifetime/ExpressionLifetimeDescriptor.cs index ea3a3e01..b5b0fed6 100644 --- a/src/Lifetime/ExpressionLifetimeDescriptor.cs +++ b/src/Lifetime/ExpressionLifetimeDescriptor.cs @@ -14,17 +14,23 @@ public abstract class ExpressionLifetimeDescriptor : LifetimeDescriptor ResolutionContext resolutionContext, TypeInformation typeInformation) { var expression = GetExpressionForRegistration(serviceRegistration, resolutionContext, typeInformation); - return expression == null ? null : this.ApplyLifetime(expression, serviceRegistration, resolutionContext, typeInformation.Type); + return this.ApplyLifetimeToExpression(expression, serviceRegistration, resolutionContext, typeInformation); } + internal override Expression? ApplyLifetimeToExpression(Expression? expression, + ServiceRegistration serviceRegistration, + ResolutionContext resolutionContext, TypeInformation typeInformation) => expression == null + ? null + : this.ApplyLifetime(expression, serviceRegistration, resolutionContext, typeInformation); + /// /// Derived types are using this method to apply their lifetime to the instance creation. /// /// The expression the lifetime should apply to. /// The service registration. /// The info about the actual resolution. - /// The type of the resolved service. + /// The type information of the resolved service. /// The lifetime managed expression. protected abstract Expression ApplyLifetime(Expression expression, ServiceRegistration serviceRegistration, - ResolutionContext resolutionContext, Type resolveType); + ResolutionContext resolutionContext, TypeInformation typeInformation); } \ No newline at end of file diff --git a/src/Lifetime/FactoryLifetimeDescriptor.cs b/src/Lifetime/FactoryLifetimeDescriptor.cs index e0dc9e9e..7d575f3a 100644 --- a/src/Lifetime/FactoryLifetimeDescriptor.cs +++ b/src/Lifetime/FactoryLifetimeDescriptor.cs @@ -14,8 +14,15 @@ public abstract class FactoryLifetimeDescriptor : LifetimeDescriptor ResolutionContext resolutionContext, TypeInformation typeInformation) { var factory = GetFactoryDelegateForRegistration(serviceRegistration, resolutionContext, typeInformation); - return factory == null ? null : this.ApplyLifetime(factory, serviceRegistration, resolutionContext, typeInformation.Type); + return factory == null ? null : this.ApplyLifetime(factory, serviceRegistration, resolutionContext, typeInformation); } + + internal override Expression? ApplyLifetimeToExpression(Expression? expression, + ServiceRegistration serviceRegistration, + ResolutionContext resolutionContext, TypeInformation typeInformation) => expression == null + ? null + : this.ApplyLifetime(expression.CompileDelegate(resolutionContext, resolutionContext.CurrentContainerContext.ContainerConfiguration), + serviceRegistration, resolutionContext, typeInformation); private static Func? GetFactoryDelegateForRegistration(ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, TypeInformation typeInformation) @@ -46,8 +53,8 @@ public abstract class FactoryLifetimeDescriptor : LifetimeDescriptor /// The factory which can be used to instantiate the service. /// The service registration. /// The info about the actual resolution. - /// The type of the resolved service. + /// The type information of the resolved service. /// The lifetime managed expression. protected abstract Expression ApplyLifetime(Func factory, ServiceRegistration serviceRegistration, - ResolutionContext resolutionContext, Type resolveType); + ResolutionContext resolutionContext, TypeInformation typeInformation); } \ No newline at end of file diff --git a/src/Lifetime/LifetimeDescriptor.cs b/src/Lifetime/LifetimeDescriptor.cs index 7f3aadef..bf915aeb 100644 --- a/src/Lifetime/LifetimeDescriptor.cs +++ b/src/Lifetime/LifetimeDescriptor.cs @@ -12,14 +12,13 @@ namespace Stashbox.Lifetime; /// public abstract class LifetimeDescriptor { - - private protected virtual bool StoreResultInLocalVariable => false; + internal virtual bool StoreResultInLocalVariable => false; /// /// An indicator used to validate the lifetime configuration of the resolution tree. /// Services with longer life-span shouldn't contain dependencies with shorter ones. /// - protected virtual int LifeSpan => 0; + protected internal virtual int LifeSpan => 0; /// /// The name of the lifetime, used only for diagnostic reasons. @@ -50,6 +49,11 @@ protected LifetimeDescriptor() $"{serviceRegistration.ImplementationType} ({this.Name}|{this.LifeSpan})"); } + if (resolutionContext.AutoLifetimeTracking != null && this.LifeSpan > resolutionContext.AutoLifetimeTracking.HighestRankingLifetime.LifeSpan) + { + resolutionContext.AutoLifetimeTracking.HighestRankingLifetime = this; + } + if (!this.StoreResultInLocalVariable) return this.BuildLifetimeAppliedExpression(serviceRegistration, resolutionContext, typeInformation); @@ -72,6 +76,9 @@ protected LifetimeDescriptor() private protected abstract Expression? BuildLifetimeAppliedExpression(ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, TypeInformation typeInformation); + + internal abstract Expression? ApplyLifetimeToExpression(Expression? expression, ServiceRegistration serviceRegistration, + ResolutionContext resolutionContext, TypeInformation typeInformation); private protected static Expression? GetExpressionForRegistration(ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, TypeInformation typeInformation) diff --git a/src/Lifetime/Lifetimes.cs b/src/Lifetime/Lifetimes.cs index 0a0bea8d..514a4ad9 100644 --- a/src/Lifetime/Lifetimes.cs +++ b/src/Lifetime/Lifetimes.cs @@ -33,7 +33,17 @@ public static class Lifetimes /// /// Produces a NamedScope lifetime. /// + /// The name of the scope. + /// A named-scope lifetime. public static LifetimeDescriptor NamedScope(object name) => new NamedScopeLifetime(name); + /// + /// Produces a lifetime that aligns to the lifetime of the resolved service's dependencies. + /// When the underlying service has a dependency with a higher lifespan, this lifetime will inherit that lifespan up to a given boundary. + /// + /// The lifetime that represents a boundary which the derived lifetime must not exceed. + /// An auto lifetime. + public static LifetimeDescriptor Auto(LifetimeDescriptor boundaryLifetime) => new AutoLifetime(boundaryLifetime); + internal static readonly LifetimeDescriptor Empty = new EmptyLifetime(); } \ No newline at end of file diff --git a/src/Lifetime/NamedScopeLifetime.cs b/src/Lifetime/NamedScopeLifetime.cs index fd825062..b7ef86a0 100644 --- a/src/Lifetime/NamedScopeLifetime.cs +++ b/src/Lifetime/NamedScopeLifetime.cs @@ -21,7 +21,7 @@ public class NamedScopeLifetime : FactoryLifetimeDescriptor public readonly object ScopeName; /// - protected override int LifeSpan => 10; + protected internal override int LifeSpan => 10; /// /// Constructs a . @@ -34,7 +34,7 @@ public NamedScopeLifetime(object scopeName) /// protected override Expression ApplyLifetime(Func factory, - ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, Type resolveType) => + ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, TypeInformation typeInformation) => GetScopeValueMethod.CallStaticMethod(resolutionContext.CurrentScopeParameter, resolutionContext.RequestContextParameter, factory.AsConstant(), diff --git a/src/Lifetime/PerRequestLifetime.cs b/src/Lifetime/PerRequestLifetime.cs index 54745c2a..b775c233 100644 --- a/src/Lifetime/PerRequestLifetime.cs +++ b/src/Lifetime/PerRequestLifetime.cs @@ -12,11 +12,11 @@ namespace Stashbox.Lifetime; public class PerRequestLifetime : FactoryLifetimeDescriptor { /// - private protected override bool StoreResultInLocalVariable => true; + internal override bool StoreResultInLocalVariable => true; /// protected override Expression ApplyLifetime(Func factory, - ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, Type resolveType) + ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, TypeInformation typeInformation) { resolutionContext.RequestConfiguration.RequiresRequestContext = true; diff --git a/src/Lifetime/PerScopedRequestLifetime.cs b/src/Lifetime/PerScopedRequestLifetime.cs index fc336711..984c1f68 100644 --- a/src/Lifetime/PerScopedRequestLifetime.cs +++ b/src/Lifetime/PerScopedRequestLifetime.cs @@ -6,5 +6,5 @@ public class PerScopedRequestLifetime : TransientLifetime { /// - private protected override bool StoreResultInLocalVariable => true; + internal override bool StoreResultInLocalVariable => true; } \ No newline at end of file diff --git a/src/Lifetime/ScopedLifetime.cs b/src/Lifetime/ScopedLifetime.cs index 6909f51f..65d096da 100644 --- a/src/Lifetime/ScopedLifetime.cs +++ b/src/Lifetime/ScopedLifetime.cs @@ -13,14 +13,14 @@ namespace Stashbox.Lifetime; public class ScopedLifetime : FactoryLifetimeDescriptor { /// - protected override int LifeSpan => 10; + protected internal override int LifeSpan => 10; /// - private protected override bool StoreResultInLocalVariable => true; + internal override bool StoreResultInLocalVariable => true; /// protected override Expression ApplyLifetime(Func factory, - ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, Type resolveType) + ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, TypeInformation typeInformation) { if (resolutionContext.CurrentContainerContext.ContainerConfiguration.LifetimeValidationEnabled && resolutionContext.IsRequestedFromRoot) diff --git a/src/Lifetime/SingletonLifetime.cs b/src/Lifetime/SingletonLifetime.cs index 2b6c8821..0853a9fd 100644 --- a/src/Lifetime/SingletonLifetime.cs +++ b/src/Lifetime/SingletonLifetime.cs @@ -12,14 +12,14 @@ namespace Stashbox.Lifetime; public class SingletonLifetime : FactoryLifetimeDescriptor { /// - protected override int LifeSpan => 20; + protected internal override int LifeSpan => 20; /// - private protected override bool StoreResultInLocalVariable => true; + internal override bool StoreResultInLocalVariable => true; /// protected override Expression ApplyLifetime(Func factory, - ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, Type resolveType) + ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, TypeInformation typeInformation) { var rootScope = resolutionContext.RequestInitiatorContainerContext.ContainerConfiguration.ReBuildSingletonsInChildContainerEnabled ? resolutionContext.RequestInitiatorContainerContext.RootScope diff --git a/src/Lifetime/TransientLifetime.cs b/src/Lifetime/TransientLifetime.cs index ebee9f6a..6cad5ab4 100644 --- a/src/Lifetime/TransientLifetime.cs +++ b/src/Lifetime/TransientLifetime.cs @@ -12,6 +12,6 @@ public class TransientLifetime : ExpressionLifetimeDescriptor { /// protected override Expression ApplyLifetime(Expression expression, - ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, Type resolveType) => + ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, TypeInformation typeInformation) => expression; } \ No newline at end of file diff --git a/src/Registration/Fluent/BaseFluentConfigurator.cs b/src/Registration/Fluent/BaseFluentConfigurator.cs index 61d5dde0..079bb830 100644 --- a/src/Registration/Fluent/BaseFluentConfigurator.cs +++ b/src/Registration/Fluent/BaseFluentConfigurator.cs @@ -99,6 +99,14 @@ public TConfigurator WithLifetime(LifetimeDescriptor lifetime) /// /// The configurator itself. public TConfigurator WithPerRequestLifetime() => this.WithLifetime(Lifetimes.PerRequest); + + /// + /// Sets the lifetime to auto lifetime. This lifetime aligns to the lifetime of the resolved service's dependencies. + /// When the underlying service has a dependency with a higher lifespan, this lifetime will inherit that lifespan up to a given boundary. + /// + /// The lifetime that represents a boundary which the derived lifetime must not exceed. + /// The configurator itself. + public TConfigurator WithAutoLifetime(LifetimeDescriptor boundaryLifetime) => this.WithLifetime(Lifetimes.Auto(boundaryLifetime)); /// /// Sets a scope name condition for the registration, it will be used only when a scope with the given name requests it. diff --git a/src/Resolution/ResolutionContext.cs b/src/Resolution/ResolutionContext.cs index 1b226fc2..03a3317b 100644 --- a/src/Resolution/ResolutionContext.cs +++ b/src/Resolution/ResolutionContext.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Linq.Expressions; using System.Runtime.CompilerServices; +using Stashbox.Lifetime; namespace Stashbox.Resolution; @@ -17,8 +18,13 @@ public class ResolutionContext { internal class PerRequestConfiguration { - public bool RequiresRequestContext { get; set; } - public bool FactoryDelegateCacheEnabled { get; set; } + public bool RequiresRequestContext; + public bool FactoryDelegateCacheEnabled; + } + + internal class AutoLifetimeTracker + { + public LifetimeDescriptor HighestRankingLifetime = Lifetimes.Transient; } private readonly bool shouldFallBackToRequestInitiatorContext; @@ -42,6 +48,7 @@ internal class PerRequestConfiguration internal readonly bool UnknownTypeCheckDisabled; internal readonly RequestContext RequestContext; internal readonly bool IsValidationContext; + internal readonly AutoLifetimeTracker? AutoLifetimeTracking; /// /// True if null result is allowed, otherwise false. @@ -101,6 +108,7 @@ private ResolutionContext(IEnumerable initialScopeNames, this.RequestConfiguration.FactoryDelegateCacheEnabled = this.PerResolutionRequestCacheEnabled = dependencyOverrides == null; this.RequestContext = dependencyOverrides != null ? RequestContext.FromOverrides(dependencyOverrides) : RequestContext.Begin(); this.IsValidationContext = isValidationContext; + this.AutoLifetimeTracking = null; this.ExpressionOverrides = dependencyOverrides == null && (knownInstances == null || knownInstances.IsEmpty) ? null @@ -128,6 +136,7 @@ private ResolutionContext(PerRequestConfiguration perRequestConfiguration, HashTree? expressionOverrides, ExpandableArray[]> parameterExpressions, RequestContext requestContext, + AutoLifetimeTracker? autoLifetimeTracker, ResolutionBehavior resolutionBehavior, ResolutionBehavior requestInitiatorResolutionBehavior, bool nullResultAllowed, @@ -165,6 +174,7 @@ private ResolutionContext(PerRequestConfiguration perRequestConfiguration, this.IsValidationContext = isValidationContext; this.ResolutionBehavior = resolutionBehavior; this.RequestInitiatorResolutionBehavior = requestInitiatorResolutionBehavior; + this.AutoLifetimeTracking = autoLifetimeTracker; } /// @@ -262,6 +272,12 @@ internal ResolutionContext BeginDecoratingContext(Type decoratingType, IEnumerab internal ResolutionContext BeginLifetimeValidationContext(int lifeSpan, string currentlyLifeSpanValidatingService) => this.Clone(currentLifeSpan: lifeSpan, nameOfServiceLifeSpanValidatingAgainst: currentlyLifeSpanValidatingService); + internal ResolutionContext BeginAutoLifetimeTrackingContext(AutoLifetimeTracker autoLifetimeTracker) => + this.Clone(autoLifetimeTracker: autoLifetimeTracker, + definedVariables: new Tree(), + singleInstructions: new ExpandableArray(), + cachedExpressions: new Tree()); + private static HashTree ProcessDependencyOverrides(object[]? dependencyOverrides, ImmutableTree? knownInstances) { var result = new HashTree(); @@ -297,6 +313,7 @@ private ResolutionContext Clone( IContainerContext? currentContainerContext = null, ExpandableArray[]>? parameterExpressions = null, ResolutionBehavior? resolutionBehavior = null, + AutoLifetimeTracker? autoLifetimeTracker = null, string? nameOfServiceLifeSpanValidatingAgainst = null, int? currentLifeSpan = null, bool? perResolutionRequestCacheEnabled = null, @@ -318,6 +335,7 @@ private ResolutionContext Clone( this.ExpressionOverrides, parameterExpressions ?? this.ParameterExpressions, this.RequestContext, + autoLifetimeTracker ?? this.AutoLifetimeTracking, resolutionBehavior ?? this.ResolutionBehavior, this.RequestInitiatorResolutionBehavior, this.NullResultAllowed, diff --git a/test/LifetimeTests.cs b/test/LifetimeTests.cs index 2ee6f8a1..ec6bdae3 100644 --- a/test/LifetimeTests.cs +++ b/test/LifetimeTests.cs @@ -492,6 +492,61 @@ public void LifetimeTests_PerRequest_With_Singleton() Assert.Same(inst.Test5, inst.Test6.Test5); } + + [Theory] + [ClassData(typeof(CompilerTypeTestData))] + public void LifetimeTests_AutoLifetime(CompilerType compilerType) + { + using IStashboxContainer container = new StashboxContainer(c => c.WithCompiler(compilerType)); + + container.Register(); + container.Register(c => c.WithSingletonLifetime()); + container.Register(c => c.WithAutoLifetime(Lifetimes.Singleton)); + + Assert.Same(container.Resolve(), container.Resolve()); + } + + [Theory] + [ClassData(typeof(CompilerTypeTestData))] + public void LifetimeTests_AutoLifetime_Scoped(CompilerType compilerType) + { + using IStashboxContainer container = new StashboxContainer(c => c.WithCompiler(compilerType)); + + container.Register(); + container.Register(c => c.WithSingletonLifetime()); + container.Register(c => c.WithAutoLifetime(Lifetimes.Scoped)); + + Assert.NotSame(container.BeginScope().Resolve(), container.BeginScope().Resolve()); + + var scope = container.BeginScope(); + Assert.Same(scope.Resolve(), scope.Resolve()); + } + + [Theory] + [ClassData(typeof(CompilerTypeTestData))] + public void LifetimeTests_AutoLifetime_Transient(CompilerType compilerType) + { + using IStashboxContainer container = new StashboxContainer(c => c.WithCompiler(compilerType)); + + container.Register(); + container.Register(c => c.WithSingletonLifetime()); + container.Register(c => c.WithAutoLifetime(Lifetimes.Transient)); + + Assert.NotSame(container.Resolve(), container.Resolve()); + } + + [Theory] + [ClassData(typeof(CompilerTypeTestData))] + public void LifetimeTests_AutoLifetime_Remains_Transient(CompilerType compilerType) + { + using IStashboxContainer container = new StashboxContainer(c => c.WithCompiler(compilerType)); + + container.Register(); + container.Register(); + container.Register(c => c.WithAutoLifetime(Lifetimes.Singleton)); + + Assert.NotSame(container.Resolve(), container.Resolve()); + } interface ITest1 { string Name { get; set; } }