Add policy factory overloads for pooled instance registration#14
Conversation
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
1ba32c9 to
0bbc5f3
Compare
|
Thank you for putting this together — and for sticking with the conversation while we worked out exactly what you were after. You were right that there's a real gap here, and I appreciate the effort you put into the implementation and tests. I'd like to help get this merged, but I think we'll get there faster if we narrow the scope a bit. Here's how I'm seeing it: The part I'd love to takeThe core of your request — being able to supply the pooling policy lazily from the container instead of as an already-built instance — is a great fit. It's the same "give me the recipe, not the instance" pattern we use throughout Autofac, and it solves your cache-backed scenario cleanly. Concretely, that's a deferred overload of the existing methods: PooledInstancePerLifetimeScope(
this IRegistrationBuilder<...> registration,
Func<IComponentContext, IPooledRegistrationPolicy<TLimit>> policyFactory)(plus the matching A few implementation notes when you get to it:
The part I'd like to set aside for nowThe fully custom Example: your cache-backed scenario, no custom pool neededThe key insight is that the policy is the right place for cache-backed behavior, and as a constructor dependency, just like any other component. Your policy decides what public sealed class CacheBackedPolicy<T> : DefaultPooledRegistrationPolicy<T>
where T : class
{
private readonly IMemoryCache _cache;
public CacheBackedPolicy(IMemoryCache cache) => _cache = cache;
public override T Get(IComponentContext context, IEnumerable<Parameter> parameters, Func<T> getFromPool)
{
// Hand back a cached instance if one is available; otherwise pull from the pool.
if (_cache.TryGetValue(typeof(T), out T cached) && cached is not null)
{
_cache.Remove(typeof(T));
return cached;
}
return getFromPool();
}
public override bool Return(T pooledObject)
{
// Park the instance in the cache with whatever eviction policy you like
// (sliding expiration for idle time, size limits for pressure, etc.).
_cache.Set(typeof(T), pooledObject, new MemoryCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromMinutes(5),
Size = 1,
});
// Returning false means "I've taken ownership of this instance" — the pool
// won't retain it, because the cache is now managing its lifetime.
return false;
}
}And registration ties it together — note the cache is registered normally, and the policy factory resolves it at build time, which is exactly the ordering problem you ran into: builder.RegisterType<MemoryCache>()
.As<IMemoryCache>()
.SingleInstance();
builder.RegisterType<Hello>()
.PooledInstancePerLifetimeScope(ctx =>
new CacheBackedPolicy<Hello>(ctx.Resolve<IMemoryCache>()));That gives you a generic, cache-driven pooling strategy that's resolved from the container, without Autofac having to hand off the whole Suggested path forwardCould we trim this PR down to just the policyFactory overloads (with the existing method names) and a couple of focused tests? That's something I can review and merge quickly. The custom-pool idea can live as its own discussion, and if we land on a good design it becomes a clean follow-up PR. None of this is a reflection on the work — it's solid, and the underlying idea is one we want. I'd just rather ship the high-value piece now and give the bigger idea the consideration it deserves. Thanks again for contributing! |
|
Actually, I still want the In my view, the semantics of an object pool should be the same as a cache. I'm not satisfied with Microsoft's default If you only need the The problem was that my English isn't good, so I relied on AI to analyze the project. I didn't realize that If you don't accept the Still, the same issue remains: the content was created by AI and needs review. |
|
That's really helpful context — thank you for being so candid about how the PR came together. And to be clear up front: I'm not saying no to Your motivation is completely legitimate. "I don't want Microsoft's default The reason I want to slow down is that handing Autofac a fully custom
None of these are dealbreakers — they're just open questions, and I don't think it's fair to you or to the project to merge the custom-pool API before we've answered them. That's design work worth doing in its own issue, where we can talk through the contract before writing more code. So here's what I'd suggest, and I think it's genuinely good news for you:
This isn't the door closing on the custom pool; it's us taking it one solid step at a time so each piece is something we're confident owning long-term. I'd really like to get your name on both. And no worries at all about the AI-assisted parts — that's exactly what review is for. Thanks for pushing on this. |
…> factory Replace the Func<IComponentContext, ObjectPool<TLimit>> overload (PooledInstancePerLifetimeScope) with Func<IComponentContext, IPooledRegistrationPolicy<TLimit>>, wrapping the resolved policy in DefaultObjectPoolProvider + AutofacPooledObjectPolicy to align with the existing pooled registration paths. - PoolActivator: _poolFactory → _policyFactory; pipeline wraps policy in DefaultObjectPoolProvider + AutofacPooledObjectPolicy - PoolGetActivator: add _policyFactory constructor + resolve at pipeline - RegistrationExtensions: add PooledInstancePerLifetimeScope(policyFactory) and PooledInstancePerMatchingLifetimeScope(policyFactory, tags) - DelegatingPooledRegistrationPolicy: removed (no longer needed) - Tests: rewritten to cover policy factory path
This reverts commit 0bbc5f3.
… builder" This reverts commit 596999e.
|
Thank you so much for your detailed response. The thorough context translated your statements with remarkable precision. I believe this commit should now be exactly what you're looking for. |
tillig
left a comment
There was a problem hiding this comment.
This is a big step in the right direction — thank you for reworking it. Dropping the custom ObjectPool<T> path and scoping down to policyFactory overloads on the existing PooledInstancePerLifetimeScope / PooledInstancePerMatchingLifetimeScope methods is exactly the shape we were hoping for. The API naming and vocabulary are consistent with what's already there, the metadata leak is gone, the factory is passed as a typed constructor argument, and there's no dead code this time. The direction is right and this is close.
I'm requesting changes on the following — I'd like all of these addressed before merge:
- Resolve the policy once and share that single instance between pool creation (
PoolActivator) and the get-side (PoolGetActivator). Right now the factory is invoked per-resolve on the get-side and separately when the pool is built, so a non-singleton policy ends up withGetandReturnrunning on different instances. A stateful policy would break, and the current tests don't catch it because they all useRegisterInstance. - Revert (or split out) the
OnReturnToPoolmove betweenAutofacPooledObjectPolicyandPooledInstanceTracker— it's unrelated to this feature, touches the shared return path, and may quietly change when the hook fires. - Add a non-singleton/stateful policy test (this locks down #1), plus null-argument tests, and a concurrency case. Give
PolicyFactory_PolicyAcceptsAllReturnsa real assertion or remove it. - Clean up the double blank line and confirm a green Release build via
dotnet msbuild ./default.proj. - Update the PR title and description — both still describe the removed
ObjectPool<T>design. The title ("Add ObjectPool factory overload...") should reflect what the PR now does, e.g. "Add policy factory overloads for pooled instance registration", since it becomes the squash-merge commit message and release-note entry. The body references aDelegatingPooledRegistrationPolicy<T>type and anObjectPool<TLimit>factory overload, neither of which exists in the current diff — please update it to describe theFunc<IComponentContext, IPooledRegistrationPolicy<TLimit>>overloads that are actually here.
Once the policy-instance handling is sorted and the unrelated refactor is out, I'm happy to merge. Really appreciate how you've engaged with the feedback on this — the design is in good shape now. And as discussed, the custom-pool idea stays alive in #6 as a deliberate follow-up.
… move, add tests Review comments addressed: 1. Share policy instance: PoolActivator calls policyFactory once, stores the resolved policy in a new PooledInstanceContext<T> alongside the pool. PoolGetActivator extracts the policy from the context instead of calling the factory per-resolve. This ensures Get and Return operate on the same policy instance. 2. Revert OnReturnToPool move: Restore OnReturnToPool() call in AutofacPooledObjectPolicy.Return() and remove it from PooledInstanceTracker.Dispose(). This change is unrelated to the factory feature and will be handled in a separate PR if needed. 3. Tests: add non-singleton stateful policy test, null-argument tests for both overloads, and a concurrency test. Removed the weak PolicyFactory_PolicyAcceptsAllReturns test. 4. Clean up double blank line in RegistrationExtensions.cs. 5. PooledInstanceContext.cs: new internal type for sharing the resolved policy instance between pool creation and retrieval.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #14 +/- ##
===========================================
- Coverage 86.19% 86.18% -0.01%
===========================================
Files 8 9 +1
Lines 210 333 +123
Branches 19 32 +13
===========================================
+ Hits 181 287 +106
- Misses 18 26 +8
- Partials 11 20 +9 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Hi, this was created with AI assistance to address issue #6. However, I'm not very familiar with this project, so I'd appreciate a review to confirm everything is correct.
Add new
PooledInstancePerLifetimeScope(Func<IComponentContext, IPooledRegistrationPolicy<TLimit>>)andPooledInstancePerMatchingLifetimeScope(Func<IComponentContext, IPooledRegistrationPolicy<TLimit>>, params object[])overloads that accept a factory delegate, allowing the pooling policy to be resolved from the container at registration time rather than supplied as an already-built instance.This enables policies with container-managed dependencies (e.g., a cache-backed policy that resolves
IMemoryCachefrom DI).Design
The factory is called once when the pool is built, and the resulting policy instance is shared between the pool's create/return path and the get-side resolve path via a new internal
PooledInstanceContext<T>type. This preserves the behavior of the existingIPooledRegistrationPolicy<T>overloads — a single policy object handles bothGetandReturn.The factory is passed as a typed constructor argument to the activator (not via registration metadata), consistent with the existing overloads.
Changes
PoolActivator<T>: new constructor accepting(Service, Func<IComponentContext, IPooledRegistrationPolicy<TLimit>>); pipeline calls factory once and wraps the pool + policy inPooledInstanceContext<T>PoolGetActivator<T>: new constructor accepting onlyPoolService; extracts shared policy fromPooledInstanceContext<T>at resolve time (no per-resolve factory invocation)PooledInstanceContext<T>: new internal type that bundles the resolved pool and policy for sharing between activatorsRegistrationExtensions: 2 new public overloads —PooledInstancePerLifetimeScope(policyFactory)andPooledInstancePerMatchingLifetimeScope(policyFactory, tags); 1 privateRegisterPooled(policyFactory, tags)methodTests
9 new tests covering:
IComponentContextwith access to registered services