Skip to content

Commit 1ba32c9

Browse files
committed
Add ObjectPool<T> factory overload for pooled instance registration
Add a new PooledInstancePerLifetimeScope(Func<IComponentContext, ObjectPool<TLimit>>) overload that accepts a factory delegate, allowing the ObjectPool<T> to be resolved from the container at resolve time rather than created during registration. This enables custom pools (e.g. CacheObjectPool<T>) to have their dependencies managed by Autofac DI. Key changes: - New DelegatingPooledRegistrationPolicy<T> as pass-through policy for factory path - PoolActivator<T> now supports factory constructor; branches at resolve time - IPooledComponent.OnReturnToPool() moved to PooledInstanceTracker.Dispose() so it fires regardless of pool implementation - RegistrationExtensions: 1 new public overload + 1 private RegisterPooled overload - 6 new tests covering factory registration, reuse, and lifecycle callbacks
1 parent a8982d9 commit 1ba32c9

6 files changed

Lines changed: 492 additions & 15 deletions

File tree

src/Autofac.Pooling/AutofacPooledObjectPolicy.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,6 @@ public TPooledObject Create()
3939
/// <inheritdoc/>
4040
public bool Return(TPooledObject obj)
4141
{
42-
if (obj is IPooledComponent poolAwareComponent)
43-
{
44-
poolAwareComponent.OnReturnToPool();
45-
}
46-
4742
if (_servicePolicy.Return(obj))
4843
{
4944
return true;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) Autofac Project. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using Autofac.Core;
7+
8+
namespace Autofac.Pooling;
9+
10+
/// <summary>
11+
/// A simple <see cref="IPooledRegistrationPolicy{TPooledObject}"/> that delegates
12+
/// <see cref="Get(IComponentContext, IEnumerable{Parameter}, Func{TPooledObject})"/> to the
13+
/// <c>getFromPool</c> callback, allowing the underlying
14+
/// custom <c>ObjectPool&lt;T&gt;</c> to manage its own creation and
15+
/// return behavior. Used when a custom pool is provided via a factory.
16+
/// </summary>
17+
/// <typeparam name="TPooledObject">The type of object being pooled.</typeparam>
18+
internal sealed class DelegatingPooledRegistrationPolicy<TPooledObject> : IPooledRegistrationPolicy<TPooledObject>
19+
where TPooledObject : class
20+
{
21+
/// <inheritdoc/>
22+
public int MaximumRetained => int.MaxValue;
23+
24+
/// <inheritdoc/>
25+
public TPooledObject Get(IComponentContext context, IEnumerable<Parameter> parameters, Func<TPooledObject> getFromPool)
26+
{
27+
if (getFromPool is null)
28+
{
29+
throw new ArgumentNullException(nameof(getFromPool));
30+
}
31+
32+
return getFromPool();
33+
}
34+
35+
/// <inheritdoc/>
36+
public bool Return(TPooledObject pooledObject)
37+
{
38+
return true;
39+
}
40+
}

src/Autofac.Pooling/PoolActivator.cs

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ namespace Autofac.Pooling;
1515
internal sealed class PoolActivator<TLimit> : IInstanceActivator
1616
where TLimit : class
1717
{
18-
private readonly Service _pooledInstanceService;
19-
private readonly IPooledRegistrationPolicy<TLimit> _policy;
20-
private readonly DefaultObjectPoolProvider _poolProvider;
18+
private readonly Service? _pooledInstanceService;
19+
private readonly IPooledRegistrationPolicy<TLimit>? _policy;
20+
private readonly DefaultObjectPoolProvider? _poolProvider;
21+
private readonly Func<IComponentContext, ObjectPool<TLimit>>? _poolFactory;
2122

2223
/// <summary>
23-
/// Initializes a new instance of the <see cref="PoolActivator{TLimit}"/> class.
24+
/// Initializes a new instance of the <see cref="PoolActivator{TLimit}"/> class
25+
/// using the default <see cref="DefaultObjectPoolProvider"/>.
2426
/// </summary>
2527
/// <param name="pooledInstanceService">The service used to resolve new instances of the pooled registration.</param>
2628
/// <param name="policy">The pool policy.</param>
@@ -34,6 +36,20 @@ public PoolActivator(Service pooledInstanceService, IPooledRegistrationPolicy<TL
3436
};
3537
}
3638

39+
/// <summary>
40+
/// Initializes a new instance of the <see cref="PoolActivator{TLimit}"/> class
41+
/// using a factory function to create the <see cref="ObjectPool{TLimit}"/> at resolve time.
42+
/// </summary>
43+
/// <param name="poolFactory">
44+
/// A factory that returns the <see cref="ObjectPool{TLimit}"/> to use.
45+
/// Invoked during resolve, so the <see cref="IComponentContext"/> is available
46+
/// for resolving dependencies.
47+
/// </param>
48+
public PoolActivator(Func<IComponentContext, ObjectPool<TLimit>> poolFactory)
49+
{
50+
_poolFactory = poolFactory ?? throw new ArgumentNullException(nameof(poolFactory));
51+
}
52+
3753
/// <inheritdoc/>
3854
public Type LimitType { get; } = typeof(TLimit);
3955

@@ -42,15 +58,23 @@ public void ConfigurePipeline(IComponentRegistryServices componentRegistryServic
4258
{
4359
pipelineBuilder.Use(PipelinePhase.Activation, (context, next) =>
4460
{
45-
// Get a reference to the actual lifetime scope.
46-
var scope = context.Resolve<ILifetimeScope>();
61+
if (_poolFactory is not null)
62+
{
63+
// Custom factory: let the caller create the pool at resolve time.
64+
context.Instance = _poolFactory(context);
65+
}
66+
else
67+
{
68+
// Default path: use DefaultObjectPoolProvider + AutofacPooledObjectPolicy.
69+
var scope = context.Resolve<ILifetimeScope>();
4770

48-
var poolPolicy = new AutofacPooledObjectPolicy<TLimit>(_pooledInstanceService, scope, _policy);
71+
var poolPolicy = new AutofacPooledObjectPolicy<TLimit>(_pooledInstanceService!, scope, _policy!);
4972

50-
// The pool provider will create a disposable pool if the TLimit implements IDisposable.
51-
var pool = _poolProvider.Create(poolPolicy);
73+
// The pool provider will create a disposable pool if the TLimit implements IDisposable.
74+
var pool = _poolProvider!.Create(poolPolicy);
5275

53-
context.Instance = pool;
76+
context.Instance = pool;
77+
}
5478
});
5579
}
5680

src/Autofac.Pooling/PooledInstanceTracker.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ public TPooledObject Instance
3939
/// </summary>
4040
public void Dispose()
4141
{
42+
if (Instance is IPooledComponent poolAwareComponent)
43+
{
44+
poolAwareComponent.OnReturnToPool();
45+
}
46+
4247
// Put the instance back in the pool.
4348
_pool.Return(Instance);
4449
}

src/Autofac.Pooling/RegistrationExtensions.cs

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Autofac.Core.Registration;
1212
using Autofac.Core.Resolving.Middleware;
1313
using Autofac.Core.Resolving.Pipeline;
14+
using Microsoft.Extensions.ObjectPool;
1415

1516
namespace Autofac.Pooling;
1617

@@ -138,6 +139,56 @@ public static IRegistrationBuilder<TLimit, TActivatorData, TSingleRegistrationSt
138139
return registration;
139140
}
140141

142+
/// <summary>
143+
/// Configure the component so that every dependent component or manual resolve within a single <see cref="ILifetimeScope"/>
144+
/// will return the same, shared instance, retrieved from a single pool of instances shared by all lifetime scopes.
145+
/// When the scope ends, the instance will be returned to the pool.
146+
/// </summary>
147+
/// <remarks>
148+
/// <para>
149+
/// This method accepts a factory function that returns the <see cref="ObjectPool{TLimit}"/> to use. The factory is invoked
150+
/// during resolve, so you may resolve dependencies from the <see cref="IComponentContext"/> (e.g. <c>sp =&gt; sp.Resolve&lt;ObjectPool&lt;T&gt;&gt;()</c>).
151+
/// This allows the pool itself to be registered as a component and have its dependencies managed by the container.
152+
/// </para>
153+
///
154+
/// <para>
155+
/// If the <see cref="ObjectPool{TLimit}"/> created by the factory implements <see cref="IDisposable"/>, it will be disposed
156+
/// when the container is disposed. Components implementing <see cref="IPooledComponent"/> will still receive
157+
/// <see cref="IPooledComponent.OnGetFromPool"/> and <see cref="IPooledComponent.OnReturnToPool"/> lifecycle notifications.
158+
/// </para>
159+
/// </remarks>
160+
/// <typeparam name="TLimit">Registration limit type.</typeparam>
161+
/// <typeparam name="TActivatorData">Activator data type.</typeparam>
162+
/// <typeparam name="TSingleRegistrationStyle">Registration style.</typeparam>
163+
/// <param name="registration">The registration.</param>
164+
/// <param name="poolFactory">
165+
/// A factory that returns the <see cref="ObjectPool{TLimit}"/> to use for this registration.
166+
/// Invoked during resolve with access to the current <see cref="IComponentContext"/>.
167+
/// </param>
168+
/// <returns>The registration builder.</returns>
169+
public static IRegistrationBuilder<TLimit, TActivatorData, TSingleRegistrationStyle>
170+
PooledInstancePerLifetimeScope<TLimit, TActivatorData, TSingleRegistrationStyle>(
171+
this IRegistrationBuilder<TLimit, TActivatorData, TSingleRegistrationStyle> registration,
172+
Func<IComponentContext, ObjectPool<TLimit>> poolFactory)
173+
where TSingleRegistrationStyle : SingleRegistrationStyle
174+
where TActivatorData : IConcreteActivatorData
175+
where TLimit : class
176+
{
177+
if (registration == null)
178+
{
179+
throw new ArgumentNullException(nameof(registration));
180+
}
181+
182+
if (poolFactory == null)
183+
{
184+
throw new ArgumentNullException(nameof(poolFactory));
185+
}
186+
187+
RegisterPooled(registration, poolFactory, null);
188+
189+
return registration;
190+
}
191+
141192
/// <summary>
142193
/// Configure the component so that every dependent component or manual resolve within
143194
/// a <see cref="ILifetimeScope"/> tagged with any of the provided tags value gets the same, shared instance,
@@ -378,4 +429,113 @@ private static void RegisterPooled<TLimit, TActivatorData, TSingleRegistrationSt
378429

379430
callback.Callback = newCallback;
380431
}
432+
433+
private static void RegisterPooled<TLimit, TActivatorData, TSingleRegistrationStyle>(
434+
IRegistrationBuilder<TLimit, TActivatorData, TSingleRegistrationStyle> registration,
435+
Func<IComponentContext, ObjectPool<TLimit>> poolFactory,
436+
object[]? tags)
437+
where TSingleRegistrationStyle : SingleRegistrationStyle
438+
where TActivatorData : IConcreteActivatorData
439+
where TLimit : class
440+
{
441+
if (registration == null)
442+
{
443+
throw new ArgumentNullException(nameof(registration));
444+
}
445+
446+
if (poolFactory == null)
447+
{
448+
throw new ArgumentNullException(nameof(poolFactory));
449+
}
450+
451+
// Mark the lifetime appropriately.
452+
var regData = registration.RegistrationData;
453+
454+
regData.Lifetime = new PooledLifetime();
455+
regData.Sharing = InstanceSharing.None;
456+
457+
var callback = regData.DeferredCallback ?? throw new NotSupportedException(RegistrationExtensionsResources.RequiresCallbackContainer);
458+
459+
if (registration.ActivatorData.Activator is ProvidedInstanceActivator)
460+
{
461+
// Can't use provided instance activators with pooling (because it would try to repeatedly activate).
462+
throw new NotSupportedException(RegistrationExtensionsResources.CannotUseProvidedInstances);
463+
}
464+
465+
var original = callback.Callback;
466+
467+
Action<IComponentRegistryBuilder> newCallback = registry =>
468+
{
469+
// Only do the additional registrations if we are still using a PooledLifetime.
470+
if (!(regData.Lifetime is PooledLifetime))
471+
{
472+
original(registry);
473+
return;
474+
}
475+
476+
var pooledInstanceService = new UniqueService();
477+
478+
var instanceActivator = registration.ActivatorData.Activator;
479+
480+
if (registration.ResolvePipeline.Middleware.Any(c => c is CoreEventMiddleware ev && ev.EventType == ResolveEventType.OnRelease))
481+
{
482+
// OnRelease shouldn't be used with pooled instances, because if a policy chooses not to return them to the pool,
483+
// the Disposal will be fired, not the OnRelease call; this means that OnRelease wouldn't fire until the container is disposed,
484+
// which is not what we want.
485+
throw new NotSupportedException(RegistrationExtensionsResources.OnReleaseNotSupported);
486+
}
487+
488+
// First, we going to create a pooled instance activator, that will be resolved when we want to
489+
// **actually** resolve a new instance (during 'Create').
490+
// The instances themselves are owned by the pool, and will be disposed when the pool disposes
491+
// (or when the instance is not returned to the pool).
492+
var pooledInstanceRegistration = new ComponentRegistration(
493+
Guid.NewGuid(),
494+
instanceActivator,
495+
RootScopeLifetime.Instance,
496+
InstanceSharing.None,
497+
InstanceOwnership.ExternallyOwned,
498+
registration.ResolvePipeline,
499+
new[] { pooledInstanceService },
500+
new Dictionary<string, object?>());
501+
502+
registry.Register(pooledInstanceRegistration);
503+
504+
var poolService = new PoolService(pooledInstanceRegistration);
505+
506+
var poolRegistration = new ComponentRegistration(
507+
Guid.NewGuid(),
508+
new PoolActivator<TLimit>(poolFactory),
509+
RootScopeLifetime.Instance,
510+
InstanceSharing.Shared,
511+
InstanceOwnership.OwnedByLifetimeScope,
512+
new[] { poolService },
513+
new Dictionary<string, object?>());
514+
515+
registry.Register(poolRegistration);
516+
517+
var pooledGetLifetime = tags is null ? CurrentScopeLifetime.Instance : new MatchingScopeLifetime(tags);
518+
519+
// Next, create a new registration with a custom activator, that copies metadata and services from
520+
// the original registration. This registration will access the pool and return an instance from it.
521+
var poolGetRegistration = new ComponentRegistration(
522+
Guid.NewGuid(),
523+
new PoolGetActivator<TLimit>(poolService, new DelegatingPooledRegistrationPolicy<TLimit>()),
524+
pooledGetLifetime,
525+
InstanceSharing.Shared,
526+
InstanceOwnership.OwnedByLifetimeScope,
527+
regData.Services,
528+
regData.Metadata);
529+
530+
registry.Register(poolGetRegistration);
531+
532+
// 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.
533+
foreach (var srv in regData.Services)
534+
{
535+
registry.RegisterServiceMiddleware(srv, new PooledInstanceUnpackMiddleware<TLimit>(), MiddlewareInsertionMode.StartOfPhase);
536+
}
537+
};
538+
539+
callback.Callback = newCallback;
540+
}
381541
}

0 commit comments

Comments
 (0)