diff --git a/.gitignore b/.gitignore index 06a0d50..8eb87ef 100644 --- a/.gitignore +++ b/.gitignore @@ -166,3 +166,6 @@ $RECYCLE.BIN/ # JetBrains Rider .idea + +# Reasonix AI assistant +.reasonix/ diff --git a/src/Autofac.Pooling/PoolActivator.cs b/src/Autofac.Pooling/PoolActivator.cs index 9a865dd..addfe08 100644 --- a/src/Autofac.Pooling/PoolActivator.cs +++ b/src/Autofac.Pooling/PoolActivator.cs @@ -15,12 +15,14 @@ namespace Autofac.Pooling; internal sealed class PoolActivator : IInstanceActivator where TLimit : class { - private readonly Service _pooledInstanceService; - private readonly IPooledRegistrationPolicy _policy; - private readonly DefaultObjectPoolProvider _poolProvider; + private readonly Service? _pooledInstanceService; + private readonly IPooledRegistrationPolicy? _policy; + private readonly DefaultObjectPoolProvider? _poolProvider; + private readonly Func>? _policyFactory; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class + /// using the default . /// /// The service used to resolve new instances of the pooled registration. /// The pool policy. @@ -34,6 +36,23 @@ public PoolActivator(Service pooledInstanceService, IPooledRegistrationPolicy + /// Initializes a new instance of the class + /// using a factory function to create the at resolve time. + /// The default will create the backing pool. + /// + /// The service used to resolve new instances of the pooled registration. + /// + /// A factory that returns the to use. + /// Invoked during resolve, so the is available + /// for resolving dependencies. + /// + public PoolActivator(Service pooledInstanceService, Func> policyFactory) + { + _pooledInstanceService = pooledInstanceService; + _policyFactory = policyFactory ?? throw new ArgumentNullException(nameof(policyFactory)); + } + /// public Type LimitType { get; } = typeof(TLimit); @@ -42,15 +61,31 @@ public void ConfigurePipeline(IComponentRegistryServices componentRegistryServic { pipelineBuilder.Use(PipelinePhase.Activation, (context, next) => { - // Get a reference to the actual lifetime scope. - var scope = context.Resolve(); + if (_policyFactory is not null) + { + // Custom policy factory: resolve the strategy at resolve time, then create DefaultObjectPool. + var policy = _policyFactory(context); + var poolProvider = new DefaultObjectPoolProvider + { + MaximumRetained = policy.MaximumRetained, + }; + var scope = context.Resolve(); + var poolPolicy = new AutofacPooledObjectPolicy(_pooledInstanceService!, scope, policy); + var pool = poolProvider.Create(poolPolicy); + context.Instance = new PooledInstanceContext(pool, policy); + } + else + { + // Default path: use DefaultObjectPoolProvider + AutofacPooledObjectPolicy. + var scope = context.Resolve(); - var poolPolicy = new AutofacPooledObjectPolicy(_pooledInstanceService, scope, _policy); + var poolPolicy = new AutofacPooledObjectPolicy(_pooledInstanceService!, scope, _policy!); - // The pool provider will create a disposable pool if the TLimit implements IDisposable. - var pool = _poolProvider.Create(poolPolicy); + // The pool provider will create a disposable pool if the TLimit implements IDisposable. + var pool = _poolProvider!.Create(poolPolicy); - context.Instance = pool; + context.Instance = pool; + } }); } diff --git a/src/Autofac.Pooling/PoolGetActivator.cs b/src/Autofac.Pooling/PoolGetActivator.cs index 4bfad02..145dab6 100644 --- a/src/Autofac.Pooling/PoolGetActivator.cs +++ b/src/Autofac.Pooling/PoolGetActivator.cs @@ -17,7 +17,7 @@ internal sealed class PoolGetActivator : IInstanceActivator where TLimit : class { private readonly PoolService _poolService; - private readonly IPooledRegistrationPolicy _registrationPolicy; + private readonly IPooledRegistrationPolicy? _registrationPolicy; /// /// Initializes a new instance of the class. @@ -30,6 +30,16 @@ public PoolGetActivator(PoolService poolService, IPooledRegistrationPolicy + /// Initializes a new instance of the class. + /// The policy will be retrieved from the at resolve time. + /// + /// The service used to access the pool. + public PoolGetActivator(PoolService poolService) + { + _poolService = poolService; + } + /// public Type LimitType { get; } = typeof(TLimit); @@ -38,7 +48,10 @@ public void ConfigurePipeline(IComponentRegistryServices componentRegistryServic { pipelineBuilder.Use(PipelinePhase.Activation, (ctxt, next) => { - var pool = (ObjectPool)ctxt.ResolveService(_poolService); + var resolved = ctxt.ResolveService(_poolService); + var ctx = resolved as PooledInstanceContext; + var pool = ctx is not null ? ctx.Pool : (ObjectPool)resolved; + var policy = ctx is not null ? ctx.Policy : _registrationPolicy!; var didGetFromPool = false; TLimit PoolGet() @@ -47,11 +60,11 @@ TLimit PoolGet() return pool.Get(); } - var poolItem = _registrationPolicy.Get(ctxt, ctxt.Parameters, PoolGet) ?? throw new InvalidOperationException( + var poolItem = policy.Get(ctxt, ctxt.Parameters, PoolGet) ?? throw new InvalidOperationException( string.Format( CultureInfo.CurrentCulture, PoolGetActivatorResources.PolicyMustReturnInstance, - _registrationPolicy.GetType().FullName, + policy.GetType().FullName, typeof(TLimit).FullName)); if (didGetFromPool) diff --git a/src/Autofac.Pooling/PooledInstanceContext.cs b/src/Autofac.Pooling/PooledInstanceContext.cs new file mode 100644 index 0000000..b0760f2 --- /dev/null +++ b/src/Autofac.Pooling/PooledInstanceContext.cs @@ -0,0 +1,42 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Microsoft.Extensions.ObjectPool; + +namespace Autofac.Pooling; + +/// +/// Holds a resolved pool and its associated policy instance, +/// so the policy created during pool construction is shared with the get-side activator. +/// +/// The limit type of the objects in the pool. +internal sealed class PooledInstanceContext + where TLimit : class +{ + /// + /// Initializes a new instance of the class. + /// + /// The object pool. + /// The resolved registration policy. + public PooledInstanceContext(ObjectPool pool, IPooledRegistrationPolicy policy) + { + Pool = pool; + Policy = policy; + } + + /// + /// Gets the object pool. + /// + public ObjectPool Pool + { + get; + } + + /// + /// Gets the resolved registration policy, shared between pool creation and retrieval. + /// + public IPooledRegistrationPolicy Policy + { + get; + } +} diff --git a/src/Autofac.Pooling/RegistrationExtensions.cs b/src/Autofac.Pooling/RegistrationExtensions.cs index 689c4f5..13c3301 100644 --- a/src/Autofac.Pooling/RegistrationExtensions.cs +++ b/src/Autofac.Pooling/RegistrationExtensions.cs @@ -138,6 +138,58 @@ public static IRegistrationBuilder + /// Configure the component so that every dependent component or manual resolve within a single + /// will return the same, shared instance, retrieved from a single pool of instances shared by all lifetime scopes. + /// When the scope ends, the instance will be returned to the pool. + /// + /// + /// + /// This method accepts a factory function that returns the to use. + /// The factory is invoked during resolve, so you may resolve dependencies from the + /// (e.g. ctx => ctx.Resolve<IMyPolicy>()). + /// This allows the policy itself to be registered as a component and have its dependencies managed by the container. + /// + /// + /// + /// The size of the pool created with this method is equal to the + /// value returned by the factory. + /// If more instances are requested than the pool size, those instances may not be returned to the pool, + /// but will instead be disposed/discarded. + /// + /// + /// Registration limit type. + /// Activator data type. + /// Registration style. + /// The registration. + /// + /// A factory that returns the to use for this registration. + /// Invoked during resolve with access to the current . + /// + /// The registration builder. + public static IRegistrationBuilder + PooledInstancePerLifetimeScope( + this IRegistrationBuilder registration, + Func> policyFactory) + where TSingleRegistrationStyle : SingleRegistrationStyle + where TActivatorData : IConcreteActivatorData + where TLimit : class + { + if (registration == null) + { + throw new ArgumentNullException(nameof(registration)); + } + + if (policyFactory == null) + { + throw new ArgumentNullException(nameof(policyFactory)); + } + + RegisterPooled(registration, policyFactory, null); + + return registration; + } + /// /// Configure the component so that every dependent component or manual resolve within /// a tagged with any of the provided tags value gets the same, shared instance, @@ -275,6 +327,64 @@ public static IRegistrationBuilder + /// Configure the component so that every dependent component or manual resolve within + /// a tagged with any of the provided tags value gets the same, shared instance, + /// retrieved from a single pool of instances shared by all lifetime scopes. + /// When the scope ends, the instance will be returned to the pool. + /// Dependent components in lifetime scopes that are children of the tagged scope will + /// share the parent's instance. If no appropriately tagged scope can be found in the + /// hierarchy an is thrown. + /// + /// + /// + /// This method accepts a factory function that returns the to use. + /// The factory is invoked during resolve, so you may resolve dependencies from the + /// (e.g. ctx => ctx.Resolve<IMyPolicy>()). + /// This allows the policy itself to be registered as a component and have its dependencies managed by the container. + /// + /// + /// + /// The size of the pool created with this method is equal to the + /// value returned by the factory. + /// If more instances are requested than the pool size, those instances may not be returned to the pool, + /// but will instead be disposed/discarded. + /// + /// + /// Registration limit type. + /// Activator data type. + /// Registration style. + /// The registration. + /// + /// A factory that returns the to use for this registration. + /// Invoked during resolve with access to the current . + /// + /// Tags applied to matching lifetime scopes. + /// The registration builder. + public static IRegistrationBuilder + PooledInstancePerMatchingLifetimeScope( + this IRegistrationBuilder registration, + Func> policyFactory, + params object[] lifetimeScopeTags) + where TSingleRegistrationStyle : SingleRegistrationStyle + where TActivatorData : IConcreteActivatorData + where TLimit : class + { + if (registration == null) + { + throw new ArgumentNullException(nameof(registration)); + } + + if (policyFactory == null) + { + throw new ArgumentNullException(nameof(policyFactory)); + } + + RegisterPooled(registration, policyFactory, lifetimeScopeTags); + + return registration; + } + private static void RegisterPooled( IRegistrationBuilder registration, IPooledRegistrationPolicy registrationPolicy, @@ -378,4 +488,113 @@ private static void RegisterPooled( + IRegistrationBuilder registration, + Func> policyFactory, + object[]? tags) + where TSingleRegistrationStyle : SingleRegistrationStyle + where TActivatorData : IConcreteActivatorData + where TLimit : class + { + if (registration == null) + { + throw new ArgumentNullException(nameof(registration)); + } + + if (policyFactory == null) + { + throw new ArgumentNullException(nameof(policyFactory)); + } + + // Mark the lifetime appropriately. + var regData = registration.RegistrationData; + + regData.Lifetime = new PooledLifetime(); + regData.Sharing = InstanceSharing.None; + + var callback = regData.DeferredCallback ?? throw new NotSupportedException(RegistrationExtensionsResources.RequiresCallbackContainer); + + if (registration.ActivatorData.Activator is ProvidedInstanceActivator) + { + // Can't use provided instance activators with pooling (because it would try to repeatedly activate). + throw new NotSupportedException(RegistrationExtensionsResources.CannotUseProvidedInstances); + } + + var original = callback.Callback; + + Action newCallback = registry => + { + // Only do the additional registrations if we are still using a PooledLifetime. + if (!(regData.Lifetime is PooledLifetime)) + { + original(registry); + return; + } + + var pooledInstanceService = new UniqueService(); + + var instanceActivator = registration.ActivatorData.Activator; + + if (registration.ResolvePipeline.Middleware.Any(c => c is CoreEventMiddleware ev && ev.EventType == ResolveEventType.OnRelease)) + { + // OnRelease shouldn't be used with pooled instances, because if a policy chooses not to return them to the pool, + // the Disposal will be fired, not the OnRelease call; this means that OnRelease wouldn't fire until the container is disposed, + // which is not what we want. + throw new NotSupportedException(RegistrationExtensionsResources.OnReleaseNotSupported); + } + + // First, we going to create a pooled instance activator, that will be resolved when we want to + // **actually** resolve a new instance (during 'Create'). + // The instances themselves are owned by the pool, and will be disposed when the pool disposes + // (or when the instance is not returned to the pool). + var pooledInstanceRegistration = new ComponentRegistration( + Guid.NewGuid(), + instanceActivator, + RootScopeLifetime.Instance, + InstanceSharing.None, + InstanceOwnership.ExternallyOwned, + registration.ResolvePipeline, + new[] { pooledInstanceService }, + new Dictionary()); + + registry.Register(pooledInstanceRegistration); + + var poolService = new PoolService(pooledInstanceRegistration); + + var poolRegistration = new ComponentRegistration( + Guid.NewGuid(), + new PoolActivator(pooledInstanceService, policyFactory), + RootScopeLifetime.Instance, + InstanceSharing.Shared, + InstanceOwnership.OwnedByLifetimeScope, + new[] { poolService }, + new Dictionary()); + + registry.Register(poolRegistration); + + var pooledGetLifetime = tags is null ? CurrentScopeLifetime.Instance : new MatchingScopeLifetime(tags); + + // Next, create a new registration with a custom activator, that copies metadata and services from + // the original registration. This registration will access the pool and return an instance from it. + var poolGetRegistration = new ComponentRegistration( + Guid.NewGuid(), + new PoolGetActivator(poolService), + pooledGetLifetime, + InstanceSharing.Shared, + InstanceOwnership.OwnedByLifetimeScope, + regData.Services, + regData.Metadata); + + registry.Register(poolGetRegistration); + + // Finally, add a service pipeline stage to just before the sharing middleware, for each supported service, to extract the pooled instance from the pool instance container. + foreach (var srv in regData.Services) + { + registry.RegisterServiceMiddleware(srv, new PooledInstanceUnpackMiddleware(), MiddlewareInsertionMode.StartOfPhase); + } + }; + + callback.Callback = newCallback; + } } diff --git a/test/Autofac.Pooling.Test/FactoryOverloadTests.cs b/test/Autofac.Pooling.Test/FactoryOverloadTests.cs new file mode 100644 index 0000000..e8a25b7 --- /dev/null +++ b/test/Autofac.Pooling.Test/FactoryOverloadTests.cs @@ -0,0 +1,244 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Autofac.Pooling.Tests.Common; +using Xunit; + +namespace Autofac.Pooling.Test; + +public class FactoryOverloadTests +{ + [Fact] + public void PolicyFactory_RegistersAndResolves() + { + var builder = new ContainerBuilder(); + + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(ctx => new DefaultPooledRegistrationPolicy()); + + var container = builder.Build(); + + Assert.NotNull(container); + + using (var scope1 = container.BeginLifetimeScope()) + { + scope1.Resolve(); + } + + using (var scope2 = container.BeginLifetimeScope()) + { + scope2.Resolve(); + } + + container.Dispose(); + } + + [Fact] + public void PolicyFactory_ResolveUsesDefaultObjectPool() + { + var builder = new ContainerBuilder(); + + var trackingPolicy = new PoolTrackingPolicy(); + builder.RegisterInstance(trackingPolicy).As>(); + + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(ctx => ctx.Resolve>()); + + var container = builder.Build(); + + using (var scope = container.BeginLifetimeScope()) + { + var instance = scope.Resolve(); + Assert.NotNull(instance); + Assert.IsType(instance); + } + + container.Dispose(); + } + + [Fact] + public void PolicyFactory_MultipleScopesReusesInstance() + { + var builder = new ContainerBuilder(); + + var trackingPolicy = new PoolTrackingPolicy(); + builder.RegisterInstance(trackingPolicy).As>(); + + IPooledService capturedInstance = null!; + + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(ctx => ctx.Resolve>()); + + var container = builder.Build(); + + using (var scope1 = container.BeginLifetimeScope()) + { + capturedInstance = scope1.Resolve(); + Assert.Equal(1, capturedInstance.GetCalled); + } + + Assert.Equal(1, capturedInstance.ReturnCalled); + + using (var scope2 = container.BeginLifetimeScope()) + { + var instance2 = scope2.Resolve(); + Assert.Same(capturedInstance, instance2); + Assert.Equal(2, instance2.GetCalled); + } + + Assert.Equal(2, capturedInstance.ReturnCalled); + + container.Dispose(); + } + + [Fact] + public void PolicyFactory_PolicyFactoryReceivesComponentContext() + { + var builder = new ContainerBuilder(); + + var config = new PolicyConfig { MaxRetained = 16 }; + builder.RegisterInstance(config); + + var factoryCalled = false; + + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(ctx => + { + factoryCalled = true; + var cfg = ctx.Resolve(); + Assert.Equal(16, cfg.MaxRetained); + return new DefaultPooledRegistrationPolicy(cfg.MaxRetained); + }); + + var container = builder.Build(); + + using (var scope = container.BeginLifetimeScope()) + { + scope.Resolve(); + } + + Assert.True(factoryCalled); + + container.Dispose(); + } + + [Fact] + public void PolicyFactory_WithMatchingScopeTag() + { + var builder = new ContainerBuilder(); + + var trackingPolicy = new PoolTrackingPolicy(); + builder.RegisterInstance(trackingPolicy).As>(); + + builder.RegisterType() + .As() + .PooledInstancePerMatchingLifetimeScope( + ctx => ctx.Resolve>(), + "tag"); + + var container = builder.Build(); + + using (var scope = container.BeginLifetimeScope("tag")) + { + var instance = scope.Resolve(); + Assert.NotNull(instance); + } + + container.Dispose(); + } + + [Fact] + public void PolicyFactory_NonSingletonPolicySharesSameInstance() + { + var builder = new ContainerBuilder(); + + var factoryCallCount = 0; + + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(ctx => + { + factoryCallCount++; + return new PoolTrackingPolicy(); + }); + + var container = builder.Build(); + + using (var scope = container.BeginLifetimeScope()) + { + var instance = scope.Resolve(); + Assert.NotNull(instance); + } + + Assert.Equal(1, factoryCallCount); + + container.Dispose(); + } + + [Fact] + public void PolicyFactory_NullPolicyFactoryThrows() + { + var builder = new ContainerBuilder(); + + var reg = builder.RegisterType() + .As(); + + Assert.Throws(() => + reg.PooledInstancePerLifetimeScope( + (Func>)null!)); + } + + [Fact] + public void PolicyFactory_NullPolicyFactoryWithMatchingScopeThrows() + { + var builder = new ContainerBuilder(); + + var reg = builder.RegisterType() + .As(); + + Assert.Throws(() => + reg.PooledInstancePerMatchingLifetimeScope( + (Func>)null!, + "tag")); + } + + [Fact] + public async Task PolicyFactory_CanUseConcurrently() + { + var builder = new ContainerBuilder(); + + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(ctx => new DefaultPooledRegistrationPolicy()); + + var container = builder.Build(); + + var exception = await Record.ExceptionAsync(async () => + { + await Task.WhenAll(Enumerable.Range(0, 100).Select(i => Task.Run(() => + { + using var scope = container.BeginLifetimeScope(); + scope.Resolve(); + }))); + + container.Dispose(); + }); + + Assert.Null(exception); + } + + private class PolicyConfig + { + public int MaxRetained + { + get; set; + } + } +}