From 2d025b710958e77216025d883fe00427ed277df6 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Wed, 17 Jun 2026 08:09:30 -0700 Subject: [PATCH 1/8] Initial custom pool implementation. --- .pre-commit-config.yaml | 2 +- .vscode/settings.json | 4 + README.md | 86 +- src/Autofac.Pooling/PoolActivator.cs | 101 +- src/Autofac.Pooling/PoolGetActivator.cs | 26 +- src/Autofac.Pooling/PooledInstanceContext.cs | 45 +- .../Properties/AssemblyInfo.cs | 2 + src/Autofac.Pooling/RegistrationExtensions.cs | 891 +++++++++++++----- .../Autofac.Pooling.Test.csproj | 2 + .../Autofac.Pooling.Test/Common/Dependency.cs | 8 + .../Common/DependentPooledComponent.cs | 44 + .../Common/TrackingObjectPoolProvider.cs | 116 +++ .../CustomProviderTests.cs | 402 ++++++++ .../DefaultPooledRegistrationPolicyTests.cs | 72 ++ .../FactoryOverloadTests.cs | 29 + .../PoolActivatorTests.cs | 53 ++ test/Autofac.Pooling.Test/PoolServiceTests.cs | 83 ++ .../RegistrationExtensionsNullGuardTests.cs | 164 ++++ .../RegistrationExtensionsTests.cs | 108 +++ 19 files changed, 1909 insertions(+), 329 deletions(-) create mode 100644 test/Autofac.Pooling.Test/Common/Dependency.cs create mode 100644 test/Autofac.Pooling.Test/Common/DependentPooledComponent.cs create mode 100644 test/Autofac.Pooling.Test/Common/TrackingObjectPoolProvider.cs create mode 100644 test/Autofac.Pooling.Test/CustomProviderTests.cs create mode 100644 test/Autofac.Pooling.Test/DefaultPooledRegistrationPolicyTests.cs create mode 100644 test/Autofac.Pooling.Test/PoolActivatorTests.cs create mode 100644 test/Autofac.Pooling.Test/PoolServiceTests.cs create mode 100644 test/Autofac.Pooling.Test/RegistrationExtensionsNullGuardTests.cs diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8066618..527a30d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/igorshubovych/markdownlint-cli - rev: "e72a3ca1632f0b11a07d171449fe447a7ff6795e" # frozen: v0.48.0 + rev: "c7c1c7640e610068e8e4754e9f1bf109bd987dc7" # post-v0.48.0 with patches hooks: - id: markdownlint args: diff --git a/.vscode/settings.json b/.vscode/settings.json index f7754b1..765e7d3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,9 @@ "autofac", "xunit" ], + "coverage-gutters.coverageBaseDir": "artifacts/logs", + "coverage-gutters.coverageFileNames": [ + "**/coverage.cobertura.xml" + ], "omnisharp.enableEditorConfigSupport": true } diff --git a/README.md b/README.md index a364880..396dc08 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ # Autofac.Pooling -[![Build status](https://github.com/autofac/Autofac.Pooling/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/autofac/Autofac.Pooling/actions/workflows/ci.yml) [![Open in Visual Studio Code](https://open.vscode.dev/badges/open-in-vscode.svg)](https://open.vscode.dev/autofac/Autofac.Pooling) +[![Build status](https://github.com/autofac/Autofac.Pooling/actions/workflows/main.yml/badge.svg?branch=develop)](https://github.com/autofac/Autofac.Pooling/actions/workflows/main.yml) Support for pooled instance lifetime scopes in Autofac dependency injection. -Autofac can help you implement a pool of components in your application without you having to write your -own pooling implementation, and making these pooled components feel more natural in the world of DI. +Autofac can help you implement a pool of components in your application without you having to write your own pooling implementation, and making these pooled components feel more natural in the world of DI. Please file issues and pull requests for this package in this repository rather than in the Autofac core repo. @@ -15,9 +14,7 @@ Please file issues and pull requests for this package in this repository rather ## Getting Started -Once you've added a reference to the `Autofac.Pooling` package, you can start using -the new `PooledInstancePerLifetimeScope` and `PooledInstancePerMatchingLifetimeScope` -methods: +Once you've added a reference to the `Autofac.Pooling` package, you can start using the new `PooledInstancePerLifetimeScope` and `PooledInstancePerMatchingLifetimeScope` methods: ```csharp var builder = new ContainerBuilder(); @@ -52,7 +49,80 @@ using (var scope2 = container.BeginLifetimeScope()) // end of the lifetime scope. ``` +## Custom Pool Providers + +By default, pooled instances are stored in a `DefaultObjectPool` sized from the policy's `MaximumRetained` value. If you need full control over *where* instances are stored and *when* they are evicted (for example, a cache-backed pool with an idle timeout), you can supply your own `Microsoft.Extensions.ObjectPool.ObjectPoolProvider`: + +```csharp +var builder = new ContainerBuilder(); + +builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(ctx => new CacheObjectPoolProvider()); + +var container = builder.Build(); +``` + +Autofac still owns *construction* of the pooled instances (they are resolved through the container, so dependency injection and the `IPooledComponent` / `IPooledRegistrationPolicy` callbacks all work as normal). The provider only controls storage and eviction. The provider's `Create(IPooledObjectPolicy)` method is the seam: Autofac hands in its own policy whose `Create()` resolves a fully-injected instance, so your pool delegates construction to it rather than new-ing up objects itself. + +A minimal cache-backed provider looks like this: + +```csharp +public sealed class CacheObjectPoolProvider : ObjectPoolProvider +{ + // One shared cache backs every pool this provider creates. + private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions()); + + public override ObjectPool Create(IPooledObjectPolicy policy) + => new CacheObjectPool(_cache, policy); +} + +public sealed class CacheObjectPool : ObjectPool, IDisposable + where T : class +{ + private readonly IMemoryCache _cache; + private readonly IPooledObjectPolicy _policy; + + public CacheObjectPool(IMemoryCache cache, IPooledObjectPolicy policy) + { + _cache = cache; + _policy = policy; + } + + public override T Get() + // Ask the cache for a stored instance; build a new one through Autofac on a miss. + => _cache.TryGetValue(typeof(T), out T? item) && item is not null + ? item + : _policy.Create(); + + public override void Return(T obj) + { + // The policy decides whether the instance is fit to be retained. + if (_policy.Return(obj)) + { + _cache.Set(typeof(T), obj, /* eviction options */ TimeSpan.FromMinutes(5)); + } + else if (obj is IDisposable disposable) + { + // The pool owns disposal of instances it declines. + disposable.Dispose(); + } + } + + public void Dispose() => _cache.Dispose(); +} +``` + +Things to know about custom providers: + +- The provider factory is invoked **once** per registration, when the pool is built, resolved from the pool-owning (root) scope. You can resolve the provider as a singleton component and share it across several registrations (the cache-backed example above shares one cache for every type it pools). +- With a custom provider, `IPooledRegistrationPolicy.MaximumRetained` does **not** size the pool — the provider owns sizing and eviction. You can still supply a custom policy alongside the provider with the `PooledInstancePerLifetimeScope(policyFactory, providerFactory)` overload to control the `Get` / `Return` behavior. +- The pool is **shared across all lifetime scopes and threads**, so your pool must be thread-safe. +- **Disposal contract:** + - If the pool implements `IDisposable`, the container disposes it at container shutdown. + - Because `ObjectPool.Return(T)` returns `void` (no kept/dropped signal) and there is no "permanently evicted" callback, **the custom pool is responsible for disposing instances it declines on `Return` or evicts asynchronously.** Autofac cannot see those instances. + - Instances that never entered the pool (because the policy chose not to call the pool) are disposed by normal lifetime scope disposal. + ## Get Help -**Need help with Autofac?** We have [a documentation site](https://autofac.readthedocs.io/) as well as [API documentation](https://autofac.org/apidoc/). We're ready to answer your questions on [Stack Overflow](https://stackoverflow.com/questions/tagged/autofac) -or check out the [discussion forum](https://groups.google.com/forum/#forum/autofac). +**Need help with Autofac?** We have [a documentation site](https://autofac.readthedocs.io/) as well as [API documentation](https://autofac.org/apidoc/). We're ready to answer your questions on [Stack Overflow](https://stackoverflow.com/questions/tagged/autofac) or check out the [discussion forum](https://groups.google.com/forum/#forum/autofac). diff --git a/src/Autofac.Pooling/PoolActivator.cs b/src/Autofac.Pooling/PoolActivator.cs index addfe08..f2bffa4 100644 --- a/src/Autofac.Pooling/PoolActivator.cs +++ b/src/Autofac.Pooling/PoolActivator.cs @@ -9,48 +9,53 @@ namespace Autofac.Pooling; /// -/// An activator for creating new instances. +/// An activator that creates the backing a +/// pooled registration. /// -/// The limit type of the objects in the pool. +/// +/// The limit type of the objects in the pool. +/// internal sealed class PoolActivator : IInstanceActivator where TLimit : class { - private readonly Service? _pooledInstanceService; - private readonly IPooledRegistrationPolicy? _policy; - private readonly DefaultObjectPoolProvider? _poolProvider; - private readonly Func>? _policyFactory; + private readonly Service _pooledInstanceService; + private readonly Func> _policyFactory; + private readonly Func? _providerFactory; /// - /// Initializes a new instance of the class - /// using the default . + /// Initializes a new instance of the + /// class. /// - /// The service used to resolve new instances of the pooled registration. - /// The pool policy. - public PoolActivator(Service pooledInstanceService, IPooledRegistrationPolicy policy) - { - _pooledInstanceService = pooledInstanceService; - _policy = policy; - _poolProvider = new DefaultObjectPoolProvider - { - MaximumRetained = policy.MaximumRetained, - }; - } - - /// - /// 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. + /// + /// 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. + /// A factory that returns the policy to use, invoked when the pool is + /// built. + /// + /// + /// An optional factory that returns the + /// that creates the backing pool, invoked when the pool is built. When + /// , the default + /// is used, sized from + /// . /// - public PoolActivator(Service pooledInstanceService, Func> policyFactory) + /// + /// Both factories are invoked once during resolve, so the + /// is available for resolving dependencies. + /// + /// + /// Thrown when or + /// is . + /// + public PoolActivator( + Service pooledInstanceService, + Func> policyFactory, + Func? providerFactory = null) { - _pooledInstanceService = pooledInstanceService; + _pooledInstanceService = pooledInstanceService ?? throw new ArgumentNullException(nameof(pooledInstanceService)); _policyFactory = policyFactory ?? throw new ArgumentNullException(nameof(policyFactory)); + _providerFactory = providerFactory; } /// @@ -61,31 +66,21 @@ public void ConfigurePipeline(IComponentRegistryServices componentRegistryServic { pipelineBuilder.Use(PipelinePhase.Activation, (context, next) => { - 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 policy = _policyFactory(context); + var scope = context.Resolve(); + var poolPolicy = new AutofacPooledObjectPolicy(_pooledInstanceService, scope, policy); - var poolPolicy = new AutofacPooledObjectPolicy(_pooledInstanceService!, scope, _policy!); + // Use the caller's provider when one was supplied; otherwise the + // default provider sized from the policy. A custom provider owns + // sizing and eviction, so MaximumRetained is intentionally consulted + // only on the default path. The default provider produces a + // disposable pool when TLimit implements IDisposable. + var provider = _providerFactory?.Invoke(context) + ?? new DefaultObjectPoolProvider { MaximumRetained = policy.MaximumRetained }; - // The pool provider will create a disposable pool if the TLimit implements IDisposable. - var pool = _poolProvider!.Create(poolPolicy); + var pool = provider.Create(poolPolicy); - context.Instance = pool; - } + context.Instance = new PooledInstanceContext(pool, policy); }); } diff --git a/src/Autofac.Pooling/PoolGetActivator.cs b/src/Autofac.Pooling/PoolGetActivator.cs index 145dab6..aeef397 100644 --- a/src/Autofac.Pooling/PoolGetActivator.cs +++ b/src/Autofac.Pooling/PoolGetActivator.cs @@ -5,7 +5,6 @@ using System.Globalization; using Autofac.Core; using Autofac.Core.Resolving.Pipeline; -using Microsoft.Extensions.ObjectPool; namespace Autofac.Pooling; @@ -17,24 +16,16 @@ internal sealed class PoolGetActivator : IInstanceActivator where TLimit : class { private readonly PoolService _poolService; - private readonly IPooledRegistrationPolicy? _registrationPolicy; /// /// Initializes a new instance of the class. /// /// The service used to access the pool. - /// The registration policy for the pool. - public PoolGetActivator(PoolService poolService, IPooledRegistrationPolicy registrationPolicy) - { - _poolService = poolService; - _registrationPolicy = registrationPolicy; - } - - /// - /// Initializes a new instance of the class. - /// The policy will be retrieved from the at resolve time. - /// - /// The service used to access the pool. + /// + /// The pool and its policy are retrieved from the + /// resolved through + /// at resolve time. + /// public PoolGetActivator(PoolService poolService) { _poolService = poolService; @@ -48,10 +39,9 @@ public void ConfigurePipeline(IComponentRegistryServices componentRegistryServic { pipelineBuilder.Use(PipelinePhase.Activation, (ctxt, next) => { - 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 ctx = (PooledInstanceContext)ctxt.ResolveService(_poolService); + var pool = ctx.Pool; + var policy = ctx.Policy; var didGetFromPool = false; TLimit PoolGet() diff --git a/src/Autofac.Pooling/PooledInstanceContext.cs b/src/Autofac.Pooling/PooledInstanceContext.cs index b0760f2..4588c4b 100644 --- a/src/Autofac.Pooling/PooledInstanceContext.cs +++ b/src/Autofac.Pooling/PooledInstanceContext.cs @@ -1,23 +1,34 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. +using System; 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. +/// Associates a resolved object pool with the policy used to build it. /// -/// The limit type of the objects in the pool. -internal sealed class PooledInstanceContext +/// +/// The limit type of the objects in the pool. +/// +/// +/// The policy is created when the pool is built and shared with the get-side +/// activator through this wrapper, so both sides use the same policy instance. +/// +internal sealed class PooledInstanceContext : IDisposable where TLimit : class { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the + /// class. /// - /// The object pool. - /// The resolved registration policy. + /// + /// The object pool. + /// + /// + /// The resolved registration policy. + /// public PooledInstanceContext(ObjectPool pool, IPooledRegistrationPolicy policy) { Pool = pool; @@ -33,10 +44,28 @@ public ObjectPool Pool } /// - /// Gets the resolved registration policy, shared between pool creation and retrieval. + /// Gets the resolved registration policy, shared between pool creation and + /// retrieval. /// public IPooledRegistrationPolicy Policy { get; } + + /// + /// Releases the resources used by the wrapper. + /// + /// + /// The container owns this wrapper through the root scope and disposes it + /// at shutdown. Disposal cascades to when the pool + /// implements , so the pool can release any + /// instances it has retained. + /// + public void Dispose() + { + if (Pool is IDisposable disposablePool) + { + disposablePool.Dispose(); + } + } } diff --git a/src/Autofac.Pooling/Properties/AssemblyInfo.cs b/src/Autofac.Pooling/Properties/AssemblyInfo.cs index 1e1c84c..c66a4e0 100644 --- a/src/Autofac.Pooling/Properties/AssemblyInfo.cs +++ b/src/Autofac.Pooling/Properties/AssemblyInfo.cs @@ -4,9 +4,11 @@ using System; using System.Reflection; using System.Resources; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; [assembly: CLSCompliant(false)] [assembly: ComVisible(false)] [assembly: NeutralResourcesLanguage("en-US")] [assembly: AssemblyCopyright("Copyright © 2020 Autofac Contributors")] +[assembly: InternalsVisibleTo("Autofac.Pooling.Test, PublicKey=00240000048000009400000006020000002400005253413100040000010001008728425885ef385e049261b18878327dfaaf0d666dea3bd2b0e4f18b33929ad4e5fbc9087e7eda3c1291d2de579206d9b4292456abffbe8be6c7060b36da0c33b883e3878eaf7c89fddf29e6e27d24588e81e86f3a22dd7b1a296b5f06fbfb500bbd7410faa7213ef4e2ce7622aefc03169b0324bcd30ccfe9ac8204e4960be6")] diff --git a/src/Autofac.Pooling/RegistrationExtensions.cs b/src/Autofac.Pooling/RegistrationExtensions.cs index 13c3301..057d062 100644 --- a/src/Autofac.Pooling/RegistrationExtensions.cs +++ b/src/Autofac.Pooling/RegistrationExtensions.cs @@ -11,6 +11,7 @@ using Autofac.Core.Registration; using Autofac.Core.Resolving.Middleware; using Autofac.Core.Resolving.Pipeline; +using Microsoft.Extensions.ObjectPool; namespace Autofac.Pooling; @@ -20,26 +21,42 @@ namespace Autofac.Pooling; public static class RegistrationExtensions { /// - /// 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. + /// Configures the component so that every dependent component or manual + /// resolve within a single shares one instance + /// taken from a pool, returning it to the pool when the scope ends. /// + /// + /// The registration limit type. + /// + /// + /// The activator data type. + /// + /// + /// The registration style. + /// + /// + /// The registration to configure. + /// /// /// - /// The size of the pool created with this method defaults to twice the number of processors ( x 2). - /// If more instances are requested than the pool size, those instances may not be returned to the pool, but will instead be disposed/discarded. + /// The pool retains up to twice the processor count + /// ( x 2) instances. Instances + /// requested beyond that are disposed or discarded instead of being + /// retained. /// - /// /// - /// If a component needs to perform behaviour when it is retrieved from or returned to the pool, it can implement , - /// or use the overload of this method that accepts a custom . + /// To run behavior when an instance is taken from or returned to the pool, + /// implement on the component, or use an + /// overload that accepts a custom + /// . /// /// - /// Registration limit type. - /// Activator data type. - /// Registration style. - /// The registration. - /// The registration builder. + /// + /// The registration builder, to enable further configuration. + /// + /// + /// Thrown when is . + /// public static IRegistrationBuilder PooledInstancePerLifetimeScope( this IRegistrationBuilder registration) @@ -58,27 +75,44 @@ 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. + /// Configures the component so that every dependent component or manual + /// resolve within a single shares one instance + /// taken from a pool, returning it to the pool when the scope ends. /// + /// + /// The registration limit type. + /// + /// + /// The activator data type. + /// + /// + /// The registration style. + /// + /// + /// The registration to configure. + /// + /// + /// The maximum number of instances to retain in the pool. + /// /// /// - /// The size of the pool created with this method is equal to . - /// If more instances are requested than the pool size, those instances may not be returned to the pool, but will instead be disposed/discarded. + /// The pool retains up to + /// instances. Instances requested beyond that are disposed or discarded + /// instead of being retained. /// - /// /// - /// If a component needs to perform behaviour when it is retrieved from or returned to the pool, it can implement , - /// or use the overload of this method that accepts a custom . + /// To run behavior when an instance is taken from or returned to the pool, + /// implement on the component, or use an + /// overload that accepts a custom + /// . /// /// - /// Registration limit type. - /// Activator data type. - /// Registration style. - /// The registration. - /// The maximum number of instances to retain in the pool. - /// The registration builder. + /// + /// The registration builder, to enable further configuration. + /// + /// + /// Thrown when is . + /// public static IRegistrationBuilder PooledInstancePerLifetimeScope( this IRegistrationBuilder registration, @@ -98,28 +132,45 @@ 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. + /// Configures the component so that every dependent component or manual + /// resolve within a single shares one instance + /// taken from a pool governed by a custom policy, returning it to the pool + /// when the scope ends. /// + /// + /// The registration limit type. + /// + /// + /// The activator data type. + /// + /// + /// The registration style. + /// + /// + /// The registration to configure. + /// + /// + /// A custom policy that controls pool behavior. + /// /// /// - /// This method accepts a custom that provides fine-grained control of the retrieval - /// of instances from the pool, and allows the implementer to choose whether or not the instance should even be returned to the pool. + /// The policy gives fine-grained control over how instances are retrieved + /// from the pool, including whether an instance is returned to the pool at + /// all. /// - /// /// - /// The size of the pool created with this method is equal to the value on the - /// . - /// If more instances are requested than the pool size, those instances may not be returned to the pool, but will instead be disposed/discarded. + /// The pool retains up to + /// + /// instances. Instances requested beyond that are disposed or discarded + /// instead of being retained. /// /// - /// Registration limit type. - /// Activator data type. - /// Registration style. - /// The registration. - /// A custom policy for controlling pool behaviour. - /// The registration builder. + /// + /// The registration builder, to enable further configuration. + /// + /// + /// Thrown when is . + /// public static IRegistrationBuilder PooledInstancePerLifetimeScope( this IRegistrationBuilder registration, @@ -139,38 +190,209 @@ 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. + /// Configures the component so that every dependent component or manual + /// resolve within a single shares one instance + /// taken from a pool governed by a policy from the supplied factory, + /// returning it to the pool when the scope ends. /// + /// + /// The registration limit type. + /// + /// + /// The activator data type. + /// + /// + /// The registration style. + /// + /// + /// The registration to configure. + /// + /// + /// A factory that returns the policy to use, invoked when the pool is + /// built. + /// /// /// - /// 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 factory is invoked with the current , + /// so the policy can be resolved as a component and have its dependencies + /// managed by the container (for example, + /// ctx => ctx.Resolve<IMyPolicy>()). /// - /// /// - /// 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. + /// The pool retains up to the + /// value + /// returned by the factory. Instances requested beyond that are disposed or + /// discarded instead of being retained. /// /// - /// Registration limit type. - /// Activator data type. - /// Registration style. - /// The registration. + /// + /// The registration builder, to enable further configuration. + /// + /// + /// Thrown when or + /// is . + /// + 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, null); + + return registration; + } + + /// + /// Configures the component so that every dependent component or manual + /// resolve within a single shares one instance + /// taken from a pool whose storage and eviction are controlled by a custom + /// , returning it to the pool when the + /// scope ends. + /// + /// + /// The registration limit type. + /// + /// + /// The activator data type. + /// + /// + /// The registration style. + /// + /// + /// The registration to configure. + /// + /// + /// A factory that returns the that creates + /// the backing pool, invoked once when the pool is built. + /// + /// + /// + /// The factory is invoked once, resolved from the pool-owning (root) scope, + /// so it can resolve dependencies from the + /// (for example, ctx => ctx.Resolve<ObjectPoolProvider>()). + /// Autofac still owns construction of the pooled instances and the pooling + /// callbacks; the provider only controls where instances are stored and + /// when they are evicted. + /// + /// + /// Because the provider owns sizing and eviction, + /// does not + /// size the pool. The pool must be thread-safe, because it is shared + /// across all lifetime scopes and threads. + /// + /// + /// If the pool implements , the container disposes + /// it at shutdown. Because reports no + /// result and there is no eviction callback, the pool is responsible for + /// disposing instances it declines on return or evicts asynchronously. + /// + /// + /// + /// The registration builder, to enable further configuration. + /// + /// + /// Thrown when or + /// is . + /// + public static IRegistrationBuilder + PooledInstancePerLifetimeScope( + this IRegistrationBuilder registration, + Func providerFactory) + where TSingleRegistrationStyle : SingleRegistrationStyle + where TActivatorData : IConcreteActivatorData + where TLimit : class + { + if (registration == null) + { + throw new ArgumentNullException(nameof(registration)); + } + + if (providerFactory == null) + { + throw new ArgumentNullException(nameof(providerFactory)); + } + + RegisterPooled(registration, DefaultPolicyFactory.Instance, providerFactory, null); + + return registration; + } + + /// + /// Configures the component so that every dependent component or manual + /// resolve within a single shares one instance + /// taken from a pool whose behavior is controlled by a custom policy and + /// whose storage and eviction are controlled by a custom + /// , returning it to the pool when the + /// scope ends. + /// + /// + /// The registration limit type. + /// + /// + /// The activator data type. + /// + /// + /// The registration style. + /// + /// + /// The registration to configure. + /// /// - /// A factory that returns the to use for this registration. - /// Invoked during resolve with access to the current . + /// A factory that returns the policy to use, invoked when the pool is + /// built. + /// + /// + /// A factory that returns the that creates + /// the backing pool, invoked once when the pool is built. /// - /// The registration builder. + /// + /// + /// Both factories are invoked once, resolved from the pool-owning (root) + /// scope, so they can resolve dependencies from the + /// . The policy controls how instances are + /// retrieved from and returned to the pool; the provider controls where + /// instances are stored and when they are evicted. Autofac still owns + /// construction of the pooled instances and the pooling callbacks. + /// + /// + /// Because the provider owns sizing and eviction, + /// does not + /// size the pool. The pool must be thread-safe, because it is shared + /// across all lifetime scopes and threads. + /// + /// + /// If the pool implements , the container disposes + /// it at shutdown. Because reports no + /// result and there is no eviction callback, the pool is responsible for + /// disposing instances it declines on return or evicts asynchronously. + /// + /// + /// + /// The registration builder, to enable further configuration. + /// + /// + /// Thrown when , + /// , or + /// is . + /// public static IRegistrationBuilder PooledInstancePerLifetimeScope( this IRegistrationBuilder registration, - Func> policyFactory) + Func> policyFactory, + Func providerFactory) where TSingleRegistrationStyle : SingleRegistrationStyle where TActivatorData : IConcreteActivatorData where TLimit : class @@ -185,37 +407,65 @@ 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. + /// Configures the component so that every dependent component or manual + /// resolve within a tagged with any of these + /// tags shares one instance taken from a pool, returning it to the pool + /// when the scope ends. /// + /// + /// The registration limit type. + /// + /// + /// The activator data type. + /// + /// + /// The registration style. + /// + /// + /// The registration to configure. + /// + /// + /// The tags identifying the matching lifetime scopes. + /// /// /// - /// The size of the pool created with this method defaults to twice the number of processors ( x 2). - /// If more instances are requested than the pool size, those instances may not be returned to the pool, but will instead be disposed/discarded. + /// Dependent components in scopes nested below a matching scope share that + /// scope's instance. + /// + /// + /// The pool retains up to twice the processor count + /// ( x 2) instances. Instances + /// requested beyond that are disposed or discarded instead of being + /// retained. /// - /// /// - /// If a component needs to perform behaviour when it is retrieved from or returned to the pool, it can implement , - /// or use the overload of this method that accepts a custom . + /// To run behavior when an instance is taken from or returned to the pool, + /// implement on the component, or use an + /// overload that accepts a custom + /// . /// /// - /// Registration limit type. - /// Activator data type. - /// Registration style. - /// The registration. - /// Tags applied to matching lifetime scopes. - /// The registration builder. + /// + /// The registration builder, to enable further configuration. + /// + /// + /// Thrown when is . + /// + /// + /// Thrown at resolve time when no scope tagged with one of + /// exists in the hierarchy. + /// public static IRegistrationBuilder PooledInstancePerMatchingLifetimeScope( this IRegistrationBuilder registration, @@ -235,32 +485,56 @@ 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. + /// Configures the component so that every dependent component or manual + /// resolve within a tagged with any of these + /// tags shares one instance taken from a pool, returning it to the pool + /// when the scope ends. /// + /// + /// The registration limit type. + /// + /// + /// The activator data type. + /// + /// + /// The registration style. + /// + /// + /// The registration to configure. + /// + /// + /// The maximum number of instances to retain in the pool. + /// + /// + /// The tags identifying the matching lifetime scopes. + /// /// /// - /// The size of the pool created with this method is equal to . - /// If more instances are requested than the pool size, those instances may not be returned to the pool, but will instead be disposed/discarded. + /// Dependent components in scopes nested below a matching scope share that + /// scope's instance. + /// + /// + /// The pool retains up to + /// instances. Instances requested beyond that are disposed or discarded + /// instead of being retained. /// - /// /// - /// If a component needs to perform behaviour when it is retrieved from or returned to the pool, it can implement , - /// or use the overload of this method that accepts a custom . + /// To run behavior when an instance is taken from or returned to the pool, + /// implement on the component, or use an + /// overload that accepts a custom + /// . /// /// - /// Registration limit type. - /// Activator data type. - /// Registration style. - /// The registration. - /// The maximum number of instances to retain in the pool. - /// Tags applied to matching lifetime scopes. - /// The registration builder. + /// + /// The registration builder, to enable further configuration. + /// + /// + /// Thrown when is . + /// + /// + /// Thrown at resolve time when no scope tagged with one of + /// exists in the hierarchy. + /// public static IRegistrationBuilder PooledInstancePerMatchingLifetimeScope( this IRegistrationBuilder registration, @@ -281,33 +555,53 @@ 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. + /// Configures the component so that every dependent component or manual + /// resolve within a tagged with any of these + /// tags shares one instance taken from a pool governed by a custom policy, + /// returning it to the pool when the scope ends. /// + /// + /// The registration limit type. + /// + /// + /// The activator data type. + /// + /// + /// The registration style. + /// + /// + /// The registration to configure. + /// + /// + /// A custom policy that controls pool behavior. + /// + /// + /// The tags identifying the matching lifetime scopes. + /// /// /// - /// This method accepts a custom that provides fine-grained control of the retrieval - /// of instances from the pool, and allows the implementer to choose whether or not the instance should even be returned to the pool. + /// Dependent components in scopes nested below a matching scope share that + /// scope's instance. The policy gives fine-grained control over how + /// instances are retrieved from the pool, including whether an instance is + /// returned to the pool at all. /// - /// /// - /// The size of the pool created with this method is equal to the value on the - /// . - /// If more instances are requested than the pool size, those instances may not be returned to the pool, but will instead be disposed/discarded. + /// The pool retains up to + /// + /// instances. Instances requested beyond that are disposed or discarded + /// instead of being retained. /// /// - /// Registration limit type. - /// Activator data type. - /// Registration style. - /// The registration. - /// A custom policy for controlling pool behaviour. - /// Tags applied to matching lifetime scopes. - /// The registration builder. + /// + /// The registration builder, to enable further configuration. + /// + /// + /// Thrown when is . + /// + /// + /// Thrown at resolve time when no scope tagged with one of + /// exists in the hierarchy. + /// public static IRegistrationBuilder PooledInstancePerMatchingLifetimeScope( this IRegistrationBuilder registration, @@ -328,39 +622,56 @@ 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. + /// Configures the component so that every dependent component or manual + /// resolve within a tagged with any of these + /// tags shares one instance taken from a pool governed by a policy from the + /// supplied factory, returning it to the pool when the scope ends. /// + /// + /// The registration limit type. + /// + /// + /// The activator data type. + /// + /// + /// The registration style. + /// + /// + /// The registration to configure. + /// + /// + /// A factory that returns the policy to use, invoked when the pool is + /// built. + /// + /// + /// The tags identifying the matching lifetime scopes. + /// /// /// - /// 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. + /// Dependent components in scopes nested below a matching scope share that + /// scope's instance. The factory is invoked with the current + /// , so the policy can be resolved as a + /// component and have its dependencies managed by the container (for + /// example, ctx => ctx.Resolve<IMyPolicy>()). /// - /// /// - /// 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. + /// The pool retains up to the + /// value + /// returned by the factory. Instances requested beyond that are disposed or + /// discarded instead of being retained. /// /// - /// 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. + /// + /// The registration builder, to enable further configuration. + /// + /// + /// Thrown when or + /// is . + /// + /// + /// Thrown at resolve time when no scope tagged with one of + /// exists in the hierarchy. + /// public static IRegistrationBuilder PooledInstancePerMatchingLifetimeScope( this IRegistrationBuilder registration, @@ -380,132 +691,218 @@ public static IRegistrationBuilder( - IRegistrationBuilder registration, - IPooledRegistrationPolicy registrationPolicy, - object[]? tags) - where TSingleRegistrationStyle : SingleRegistrationStyle - where TActivatorData : IConcreteActivatorData - where TLimit : class + /// + /// Configures the component so that every dependent component or manual + /// resolve within a tagged with any of these + /// tags shares one instance taken from a pool whose storage and eviction + /// are controlled by a custom , returning + /// it to the pool when the scope ends. + /// + /// + /// The registration limit type. + /// + /// + /// The activator data type. + /// + /// + /// The registration style. + /// + /// + /// The registration to configure. + /// + /// + /// A factory that returns the that creates + /// the backing pool, invoked once when the pool is built. + /// + /// + /// The tags identifying the matching lifetime scopes. + /// + /// + /// + /// Dependent components in scopes nested below a matching scope share that + /// scope's instance. The factory is invoked once, resolved from the + /// pool-owning (root) scope. Autofac still owns construction of the pooled + /// instances and the pooling callbacks; the provider only controls where + /// instances are stored and when they are evicted. + /// + /// + /// Because the provider owns sizing and eviction, + /// does not + /// size the pool. The pool must be thread-safe, because it is shared + /// across all lifetime scopes and threads. + /// + /// + /// If the pool implements , the container disposes + /// it at shutdown. Because reports no + /// result and there is no eviction callback, the pool is responsible for + /// disposing instances it declines on return or evicts asynchronously. + /// + /// + /// + /// The registration builder, to enable further configuration. + /// + /// + /// Thrown when or + /// is . + /// + /// + /// Thrown at resolve time when no scope tagged with one of + /// exists in the hierarchy. + /// + public static IRegistrationBuilder + PooledInstancePerMatchingLifetimeScope( + this IRegistrationBuilder registration, + Func providerFactory, + params object[] lifetimeScopeTags) + where TSingleRegistrationStyle : SingleRegistrationStyle + where TActivatorData : IConcreteActivatorData + where TLimit : class { if (registration == null) { throw new ArgumentNullException(nameof(registration)); } - // Mark the lifetime appropriately. - var regData = registration.RegistrationData; + if (providerFactory == null) + { + throw new ArgumentNullException(nameof(providerFactory)); + } - regData.Lifetime = new PooledLifetime(); - regData.Sharing = InstanceSharing.None; + RegisterPooled(registration, DefaultPolicyFactory.Instance, providerFactory, lifetimeScopeTags); - var callback = regData.DeferredCallback ?? throw new NotSupportedException(RegistrationExtensionsResources.RequiresCallbackContainer); + return registration; + } - if (registration.ActivatorData.Activator is ProvidedInstanceActivator) + /// + /// Configures the component so that every dependent component or manual + /// resolve within a tagged with any of these + /// tags shares one instance taken from a pool whose behavior is controlled + /// by a custom policy and whose storage and eviction are controlled by a + /// custom , returning it to the pool when + /// the scope ends. + /// + /// + /// The registration limit type. + /// + /// + /// The activator data type. + /// + /// + /// The registration style. + /// + /// + /// The registration to configure. + /// + /// + /// A factory that returns the policy to use, invoked when the pool is + /// built. + /// + /// + /// A factory that returns the that creates + /// the backing pool, invoked once when the pool is built. + /// + /// + /// The tags identifying the matching lifetime scopes. + /// + /// + /// + /// Dependent components in scopes nested below a matching scope share that + /// scope's instance. Both factories are invoked once, resolved from the + /// pool-owning (root) scope. The policy controls how instances are + /// retrieved from and returned to the pool; the provider controls where + /// instances are stored and when they are evicted. Autofac still owns + /// construction of the pooled instances and the pooling callbacks. + /// + /// + /// Because the provider owns sizing and eviction, + /// does not + /// size the pool. The pool must be thread-safe, because it is shared + /// across all lifetime scopes and threads. + /// + /// + /// If the pool implements , the container disposes + /// it at shutdown. Because reports no + /// result and there is no eviction callback, the pool is responsible for + /// disposing instances it declines on return or evicts asynchronously. + /// + /// + /// + /// The registration builder, to enable further configuration. + /// + /// + /// Thrown when , + /// , or + /// is . + /// + /// + /// Thrown at resolve time when no scope tagged with one of + /// exists in the hierarchy. + /// + public static IRegistrationBuilder + PooledInstancePerMatchingLifetimeScope( + this IRegistrationBuilder registration, + Func> policyFactory, + Func providerFactory, + params object[] lifetimeScopeTags) + where TSingleRegistrationStyle : SingleRegistrationStyle + where TActivatorData : IConcreteActivatorData + where TLimit : class + { + if (registration == null) { - // Can't use provided instance activators with pooling (because it would try to repeatedly activate). - throw new NotSupportedException(RegistrationExtensionsResources.CannotUseProvidedInstances); + throw new ArgumentNullException(nameof(registration)); } - var original = callback.Callback; - - Action newCallback = registry => + if (policyFactory == null) { - // 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, registrationPolicy), - RootScopeLifetime.Instance, - InstanceSharing.Shared, - InstanceOwnership.OwnedByLifetimeScope, - new[] { poolService }, - new Dictionary()); - - registry.Register(poolRegistration); - - var pooledGetLifetime = tags is null ? CurrentScopeLifetime.Instance : new MatchingScopeLifetime(tags); + throw new ArgumentNullException(nameof(policyFactory)); + } - // 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, registrationPolicy), - pooledGetLifetime, - InstanceSharing.Shared, - InstanceOwnership.OwnedByLifetimeScope, - regData.Services, - regData.Metadata); + if (providerFactory == null) + { + throw new ArgumentNullException(nameof(providerFactory)); + } - registry.Register(poolGetRegistration); + RegisterPooled(registration, policyFactory, providerFactory, lifetimeScopeTags); - // 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; + return registration; } private static void RegisterPooled( IRegistrationBuilder registration, - Func> policyFactory, + IPooledRegistrationPolicy registrationPolicy, object[]? tags) where TSingleRegistrationStyle : SingleRegistrationStyle where TActivatorData : IConcreteActivatorData where TLimit : class { - if (registration == null) + if (registrationPolicy == null) { - throw new ArgumentNullException(nameof(registration)); + throw new ArgumentNullException(nameof(registrationPolicy)); } - if (policyFactory == null) - { - throw new ArgumentNullException(nameof(policyFactory)); - } + // A fixed policy is shared between the pool-build and get sides by + // resolving the same instance every time. + RegisterPooled(registration, _ => registrationPolicy, null, tags); + } + + private static void RegisterPooled( + IRegistrationBuilder registration, + Func> policyFactory, + Func? providerFactory, + object[]? tags) + where TSingleRegistrationStyle : SingleRegistrationStyle + where TActivatorData : IConcreteActivatorData + where TLimit : class + { + // registration and policyFactory are always validated by the public + // overloads (and PoolActivator guards policyFactory again), so no null + // checks are repeated here. // Mark the lifetime appropriately. var regData = registration.RegistrationData; @@ -564,7 +961,7 @@ private static void RegisterPooled(pooledInstanceService, policyFactory), + new PoolActivator(pooledInstanceService, policyFactory, providerFactory), RootScopeLifetime.Instance, InstanceSharing.Shared, InstanceOwnership.OwnedByLifetimeScope, @@ -597,4 +994,16 @@ private static void RegisterPooled + /// Holds a cached default policy factory per closed , + /// so the provider-only overloads do not allocate a new delegate per call. + /// + /// The registration limit type. + private static class DefaultPolicyFactory + where TLimit : class + { + public static readonly Func> Instance = + _ => new DefaultPooledRegistrationPolicy(); + } } diff --git a/test/Autofac.Pooling.Test/Autofac.Pooling.Test.csproj b/test/Autofac.Pooling.Test/Autofac.Pooling.Test.csproj index 83c24e7..74c7d0c 100644 --- a/test/Autofac.Pooling.Test/Autofac.Pooling.Test.csproj +++ b/test/Autofac.Pooling.Test/Autofac.Pooling.Test.csproj @@ -5,6 +5,8 @@ AllEnabledByDefault true false + ../../Autofac.snk + true diff --git a/test/Autofac.Pooling.Test/Common/Dependency.cs b/test/Autofac.Pooling.Test/Common/Dependency.cs new file mode 100644 index 0000000..b9fefe9 --- /dev/null +++ b/test/Autofac.Pooling.Test/Common/Dependency.cs @@ -0,0 +1,8 @@ +namespace Autofac.Pooling.Tests.Common; + +/// +/// A trivial dependency injected into . +/// +public class Dependency +{ +} diff --git a/test/Autofac.Pooling.Test/Common/DependentPooledComponent.cs b/test/Autofac.Pooling.Test/Common/DependentPooledComponent.cs new file mode 100644 index 0000000..11b9f51 --- /dev/null +++ b/test/Autofac.Pooling.Test/Common/DependentPooledComponent.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using Autofac.Core; + +namespace Autofac.Pooling.Tests.Common; + +/// +/// A pooled component with a constructor dependency, used to prove that construction of pooled +/// instances still flows through the Autofac container when a custom provider is supplied. +/// Tracks pool callbacks so tests can assert the hooks fired. +/// +public class DependentPooledComponent : IPooledService, IPooledComponent +{ + public DependentPooledComponent(Dependency dependency) + { + Dependency = dependency; + } + + public Dependency Dependency + { + get; + } + + public int GetCalled + { + get; private set; + } + + public int ReturnCalled + { + get; private set; + } + + public int DisposeCalled => 0; + + public void OnGetFromPool(IComponentContext context, IEnumerable parameters) + { + GetCalled++; + } + + public void OnReturnToPool() + { + ReturnCalled++; + } +} diff --git a/test/Autofac.Pooling.Test/Common/TrackingObjectPoolProvider.cs b/test/Autofac.Pooling.Test/Common/TrackingObjectPoolProvider.cs new file mode 100644 index 0000000..739b225 --- /dev/null +++ b/test/Autofac.Pooling.Test/Common/TrackingObjectPoolProvider.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using Microsoft.Extensions.ObjectPool; + +namespace Autofac.Pooling.Tests.Common; + +/// +/// A simple in-memory for tests that records +/// how many pools it has created and hands construction back to the supplied +/// . A single instance can be shared across +/// multiple registrations to prove provider sharing. +/// +public class TrackingObjectPoolProvider : ObjectPoolProvider +{ + private int _createCount; + + /// + /// Gets the number of times + /// has been invoked. + /// + public int CreateCount => _createCount; + + /// + public override ObjectPool Create(IPooledObjectPolicy policy) + where T : class + { + ArgumentNullException.ThrowIfNull(policy); + + Interlocked.Increment(ref _createCount); + + return new TrackingObjectPool(policy); + } + + /// + /// A thread-safe pool that delegates construction and return decisions to + /// the supplied policy. Disposes instances that the policy declines on + /// return, and disposes everything it retains when the pool itself is + /// disposed (honouring the disposal contract). + /// + /// The pooled object type. + private sealed class TrackingObjectPool : ObjectPool, IDisposable + where T : class + { + private readonly IPooledObjectPolicy _policy; + private readonly ConcurrentQueue _items = new(); + private readonly object _syncRoot = new(); + private bool _disposed; + + public TrackingObjectPool(IPooledObjectPolicy policy) + { + _policy = policy; + } + + public override T Get() + { + if (_items.TryDequeue(out var item)) + { + return item; + } + + return _policy.Create(); + } + + public override void Return(T obj) + { + if (!_policy.Return(obj)) + { + // The policy declined the instance; the pool owns disposal of + // declined instances. + Dispose(obj); + return; + } + + // Guard against a return racing with disposal: once disposed, retain + // nothing (the queue is already drained) and dispose the instance + // here so it cannot leak. + lock (_syncRoot) + { + if (_disposed) + { + Dispose(obj); + return; + } + + _items.Enqueue(obj); + } + } + + public void Dispose() + { + lock (_syncRoot) + { + if (_disposed) + { + return; + } + + _disposed = true; + } + + while (_items.TryDequeue(out var item)) + { + Dispose(item); + } + } + + private static void Dispose(T item) + { + if (item is IDisposable disposable) + { + disposable.Dispose(); + } + } + } +} diff --git a/test/Autofac.Pooling.Test/CustomProviderTests.cs b/test/Autofac.Pooling.Test/CustomProviderTests.cs new file mode 100644 index 0000000..244a709 --- /dev/null +++ b/test/Autofac.Pooling.Test/CustomProviderTests.cs @@ -0,0 +1,402 @@ +// 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 Microsoft.Extensions.ObjectPool; +using Xunit; + +namespace Autofac.Pooling.Test; + +public class CustomProviderTests +{ + [Fact] + public void Provider_RegistersAndResolves() + { + var builder = new ContainerBuilder(); + + var provider = new TrackingObjectPoolProvider(); + + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(ctx => provider); + + var container = builder.Build(); + + using (var scope = container.BeginLifetimeScope()) + { + var instance = scope.Resolve(); + Assert.NotNull(instance); + Assert.IsType(instance); + } + + container.Dispose(); + } + + [Fact] + public void Provider_BuildsThePoolExactlyOnce() + { + var builder = new ContainerBuilder(); + + var provider = new TrackingObjectPoolProvider(); + + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(ctx => provider); + + var container = builder.Build(); + + // Resolve across several scopes; the provider should only build one pool. + for (var i = 0; i < 3; i++) + { + using var scope = container.BeginLifetimeScope(); + scope.Resolve(); + } + + Assert.Equal(1, provider.CreateCount); + + container.Dispose(); + } + + [Fact] + public void Provider_ConstructionGoesThroughAutofac() + { + var builder = new ContainerBuilder(); + + var dependency = new Dependency(); + builder.RegisterInstance(dependency); + + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(ctx => new TrackingObjectPoolProvider()); + + var container = builder.Build(); + + DependentPooledComponent instance; + + using (var scope = container.BeginLifetimeScope()) + { + instance = Assert.IsType(scope.Resolve()); + + // The dependency was injected by the container, proving construction + // still flows through Autofac on the custom-provider path. + Assert.Same(dependency, instance.Dependency); + + // And the pooling hooks still fire on this path. + Assert.Equal(1, instance.GetCalled); + } + + Assert.Equal(1, instance.ReturnCalled); + + container.Dispose(); + } + + [Fact] + public void Provider_PooledComponentHooksFire() + { + var builder = new ContainerBuilder(); + + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(ctx => new TrackingObjectPoolProvider()); + + var container = builder.Build(); + + IPooledService captured; + + using (var scope = container.BeginLifetimeScope()) + { + captured = scope.Resolve(); + Assert.Equal(1, captured.GetCalled); + Assert.Equal(0, captured.ReturnCalled); + } + + // Scope disposal returns the instance to the pool, firing OnReturnToPool. + Assert.Equal(1, captured.ReturnCalled); + + using (var scope = container.BeginLifetimeScope()) + { + var reused = scope.Resolve(); + Assert.Same(captured, reused); + Assert.Equal(2, reused.GetCalled); + } + + Assert.Equal(2, captured.ReturnCalled); + + container.Dispose(); + } + + [Fact] + public void Provider_RegistrationPolicyStillApplies() + { + var builder = new ContainerBuilder(); + + var trackingPolicy = new PoolTrackingPolicy(); + var provider = new TrackingObjectPoolProvider(); + + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(ctx => trackingPolicy, ctx => provider); + + var container = builder.Build(); + + using (var scope = container.BeginLifetimeScope()) + { + scope.Resolve(); + + // The policy is mid-Get (out of the pool) during the scope. + Assert.Equal(1, trackingPolicy.OutOfPoolCount); + } + + // Return brings the count back down. + Assert.Equal(0, trackingPolicy.OutOfPoolCount); + Assert.Equal(1, provider.CreateCount); + + container.Dispose(); + } + + [Fact] + public void Provider_SharedAcrossTypesUsesSameProviderInstance() + { + var builder = new ContainerBuilder(); + + var sharedProvider = new TrackingObjectPoolProvider(); + builder.RegisterInstance(sharedProvider).As(); + + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(ctx => ctx.Resolve()); + + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(ctx => ctx.Resolve()); + + var container = builder.Build(); + + using (var scope = container.BeginLifetimeScope()) + { + scope.Resolve(); + scope.Resolve(); + } + + // One shared provider built both pools (Create and Create). + Assert.Equal(2, sharedProvider.CreateCount); + + container.Dispose(); + } + + [Fact] + public void Provider_PerTypeDifferentProvidersDoNotCrossTalk() + { + var builder = new ContainerBuilder(); + + var providerA = new TrackingObjectPoolProvider(); + var providerB = new TrackingObjectPoolProvider(); + + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(ctx => providerA); + + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(ctx => providerB); + + var container = builder.Build(); + + using (var scope = container.BeginLifetimeScope()) + { + scope.Resolve(); + scope.Resolve(); + } + + Assert.Equal(1, providerA.CreateCount); + Assert.Equal(1, providerB.CreateCount); + + container.Dispose(); + } + + [Fact] + public void Provider_MatchingScopeVariant() + { + var builder = new ContainerBuilder(); + + var provider = new TrackingObjectPoolProvider(); + + builder.RegisterType() + .As() + .PooledInstancePerMatchingLifetimeScope(ctx => provider, "tag"); + + var container = builder.Build(); + + using (var scope = container.BeginLifetimeScope("tag")) + { + var instance = scope.Resolve(); + Assert.NotNull(instance); + } + + Assert.Equal(1, provider.CreateCount); + + container.Dispose(); + } + + [Fact] + public void Provider_MatchingScopeVariantWithPolicy() + { + var builder = new ContainerBuilder(); + + var provider = new TrackingObjectPoolProvider(); + var trackingPolicy = new PoolTrackingPolicy(); + + builder.RegisterType() + .As() + .PooledInstancePerMatchingLifetimeScope(ctx => trackingPolicy, ctx => provider, "tag"); + + var container = builder.Build(); + + using (var scope = container.BeginLifetimeScope("tag")) + { + scope.Resolve(); + Assert.Equal(1, trackingPolicy.OutOfPoolCount); + } + + Assert.Equal(0, trackingPolicy.OutOfPoolCount); + Assert.Equal(1, provider.CreateCount); + + container.Dispose(); + } + + [Fact] + public void Provider_DisposableCustomPoolDisposedAtShutdown() + { + var builder = new ContainerBuilder(); + + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(ctx => new TrackingObjectPoolProvider()); + + var container = builder.Build(); + + IPooledService captured; + + using (var scope = container.BeginLifetimeScope()) + { + captured = scope.Resolve(); + } + + // Returned to the (custom, disposable) pool but not disposed yet. + Assert.Equal(0, captured.DisposeCalled); + + // Disposing the container disposes the wrapper, which cascades to the disposable pool, + // which disposes the instances it retained. + container.Dispose(); + + Assert.Equal(1, captured.DisposeCalled); + } + + [Fact] + public void Provider_NullProviderFactoryThrows() + { + var builder = new ContainerBuilder(); + + var reg = builder.RegisterType() + .As(); + + Assert.Throws(() => + reg.PooledInstancePerLifetimeScope( + (Func)null!)); + } + + [Fact] + public void Provider_NullProviderFactoryWithPolicyThrows() + { + var builder = new ContainerBuilder(); + + var reg = builder.RegisterType() + .As(); + + Assert.Throws(() => + reg.PooledInstancePerLifetimeScope( + ctx => new DefaultPooledRegistrationPolicy(), + (Func)null!)); + } + + [Fact] + public void Provider_NullPolicyFactoryWithProviderThrows() + { + var builder = new ContainerBuilder(); + + var reg = builder.RegisterType() + .As(); + + Assert.Throws(() => + reg.PooledInstancePerLifetimeScope( + (Func>)null!, + ctx => new TrackingObjectPoolProvider())); + } + + [Fact] + public void Provider_NullProviderFactoryWithMatchingScopeThrows() + { + var builder = new ContainerBuilder(); + + var reg = builder.RegisterType() + .As(); + + Assert.Throws(() => + reg.PooledInstancePerMatchingLifetimeScope( + (Func)null!, + "tag")); + } + + [Fact] + public async Task Provider_CanUseConcurrently() + { + var builder = new ContainerBuilder(); + + var provider = new TrackingObjectPoolProvider(); + + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(ctx => provider); + + 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); + } + + [Fact] + public void Provider_OverloadBindsToProviderFactory() + { + var builder = new ContainerBuilder(); + + // Register an ObjectPoolProvider so the lambda's return type is ObjectPoolProvider, + // which must bind to the provider overload rather than the policy-factory overload. + builder.RegisterInstance(new TrackingObjectPoolProvider()); + + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(ctx => ctx.Resolve()); + + var container = builder.Build(); + + using (var scope = container.BeginLifetimeScope()) + { + Assert.NotNull(scope.Resolve()); + } + + container.Dispose(); + } +} diff --git a/test/Autofac.Pooling.Test/DefaultPooledRegistrationPolicyTests.cs b/test/Autofac.Pooling.Test/DefaultPooledRegistrationPolicyTests.cs new file mode 100644 index 0000000..eddfffb --- /dev/null +++ b/test/Autofac.Pooling.Test/DefaultPooledRegistrationPolicyTests.cs @@ -0,0 +1,72 @@ +// 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 Autofac.Pooling.Tests.Common; +using Xunit; + +namespace Autofac.Pooling.Test; + +public class DefaultPooledRegistrationPolicyTests +{ + [Fact] + public void DefaultConstructorUsesTwiceProcessorCount() + { + var policy = new DefaultPooledRegistrationPolicy(); + + Assert.Equal(Environment.ProcessorCount * 2, policy.MaximumRetained); + } + + [Fact] + public void MaximumRetainedConstructorUsesSuppliedValue() + { + var policy = new DefaultPooledRegistrationPolicy(3); + + Assert.Equal(3, policy.MaximumRetained); + } + + [Fact] + public void ZeroMaximumRetainedIsAllowed() + { + var policy = new DefaultPooledRegistrationPolicy(0); + + Assert.Equal(0, policy.MaximumRetained); + } + + [Fact] + public void NegativeMaximumRetainedThrows() + { + Assert.Throws(() => + new DefaultPooledRegistrationPolicy(-1)); + } + + [Fact] + public void GetInvokesTheGetFromPoolCallback() + { + var policy = new DefaultPooledRegistrationPolicy(); + using var expected = new PooledComponent(); + + var result = policy.Get(null!, Enumerable.Empty(), () => expected); + + Assert.Same(expected, result); + } + + [Fact] + public void GetWithNullGetFromPoolThrows() + { + var policy = new DefaultPooledRegistrationPolicy(); + + Assert.Throws(() => + policy.Get(null!, Enumerable.Empty(), null!)); + } + + [Fact] + public void ReturnAlwaysAcceptsTheInstance() + { + var policy = new DefaultPooledRegistrationPolicy(); + using var instance = new PooledComponent(); + + Assert.True(policy.Return(instance)); + } +} diff --git a/test/Autofac.Pooling.Test/FactoryOverloadTests.cs b/test/Autofac.Pooling.Test/FactoryOverloadTests.cs index e8a25b7..0573e97 100644 --- a/test/Autofac.Pooling.Test/FactoryOverloadTests.cs +++ b/test/Autofac.Pooling.Test/FactoryOverloadTests.cs @@ -182,6 +182,35 @@ public void PolicyFactory_NonSingletonPolicySharesSameInstance() container.Dispose(); } + [Fact] + public void PolicyFactory_DisposableRegistrationsDisposedWhenContainerIsDisposed() + { + // Regression test: the policyFactory path stores a PooledInstanceContext wrapper as the pool + // instance. That wrapper must be IDisposable and cascade disposal to the underlying + // DisposableObjectPool, otherwise retained disposable instances leak at container shutdown. + var builder = new ContainerBuilder(); + + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(ctx => new DefaultPooledRegistrationPolicy()); + + var container = builder.Build(); + + IPooledService pooledInstance; + + using (var scope = container.BeginLifetimeScope()) + { + pooledInstance = scope.Resolve(); + } + + // Returned to the pool, but not disposed. + Assert.Equal(0, pooledInstance.DisposeCalled); + + container.Dispose(); + + Assert.Equal(1, pooledInstance.DisposeCalled); + } + [Fact] public void PolicyFactory_NullPolicyFactoryThrows() { diff --git a/test/Autofac.Pooling.Test/PoolActivatorTests.cs b/test/Autofac.Pooling.Test/PoolActivatorTests.cs new file mode 100644 index 0000000..3ca7b36 --- /dev/null +++ b/test/Autofac.Pooling.Test/PoolActivatorTests.cs @@ -0,0 +1,53 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using Autofac.Core; +using Autofac.Pooling.Tests.Common; +using Microsoft.Extensions.ObjectPool; +using Xunit; + +namespace Autofac.Pooling.Test; + +public class PoolActivatorTests +{ + [Fact] + public void NullServiceThrows() + { + Assert.Throws(() => + new PoolActivator( + null!, + _ => new DefaultPooledRegistrationPolicy())); + } + + [Fact] + public void NullPolicyFactoryThrows() + { + Assert.Throws(() => + new PoolActivator( + new UniqueService(), + null!)); + } + + [Fact] + public void ProviderFactoryIsOptional() + { + // The provider factory may be omitted; the default provider is used. + using var activator = new PoolActivator( + new UniqueService(), + _ => new DefaultPooledRegistrationPolicy()); + + Assert.Equal(typeof(PooledComponent), activator.LimitType); + } + + [Fact] + public void ExposesTheLimitType() + { + using var activator = new PoolActivator( + new UniqueService(), + _ => new DefaultPooledRegistrationPolicy(), + _ => new DefaultObjectPoolProvider()); + + Assert.Equal(typeof(PooledComponent), activator.LimitType); + } +} diff --git a/test/Autofac.Pooling.Test/PoolServiceTests.cs b/test/Autofac.Pooling.Test/PoolServiceTests.cs new file mode 100644 index 0000000..5475f73 --- /dev/null +++ b/test/Autofac.Pooling.Test/PoolServiceTests.cs @@ -0,0 +1,83 @@ +// 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 Autofac.Core; +using Autofac.Pooling.Tests.Common; +using Xunit; + +namespace Autofac.Pooling.Test; + +public class PoolServiceTests +{ + [Fact] + public void DescriptionIncludesTheLimitType() + { + var registration = RegistrationFor(); + + var service = new PoolService(registration); + + Assert.Contains(typeof(PooledComponent).FullName!, service.Description, StringComparison.Ordinal); + } + + [Fact] + public void EqualsIsTrueForTheSameUnderlyingRegistration() + { + var registration = RegistrationFor(); + + var first = new PoolService(registration); + var second = new PoolService(registration); + + Assert.True(first.Equals(second)); + Assert.Equal(first.GetHashCode(), second.GetHashCode()); + } + + [Fact] + public void EqualsIsFalseForDifferentRegistrations() + { + var first = new PoolService(RegistrationFor()); + var second = new PoolService(RegistrationFor()); + + Assert.False(first.Equals(second)); + } + + [Fact] + public void EqualsObjectMatchesAnotherPoolService() + { + var registration = RegistrationFor(); + + var first = new PoolService(registration); + object second = new PoolService(registration); + + Assert.True(first.Equals(second)); + } + + [Fact] + public void EqualsObjectIsFalseForUnrelatedType() + { + var service = new PoolService(RegistrationFor()); + + Assert.False(service.Equals("not a pool service")); + } + + [Fact] + [SuppressMessage("CA1508", "CA1508", Justification = "Deliberately exercising the Equals null branch for coverage.")] + public void EqualsIsFalseForNull() + { + var service = new PoolService(RegistrationFor()); + + Assert.False(service.Equals(null)); + } + + private static IComponentRegistration RegistrationFor() + where TComponent : notnull + { + var builder = new ContainerBuilder(); + builder.RegisterType(); + var container = builder.Build(); + + return container.ComponentRegistry.Registrations + .Single(r => r.Activator.LimitType == typeof(TComponent)); + } +} diff --git a/test/Autofac.Pooling.Test/RegistrationExtensionsNullGuardTests.cs b/test/Autofac.Pooling.Test/RegistrationExtensionsNullGuardTests.cs new file mode 100644 index 0000000..1e46a19 --- /dev/null +++ b/test/Autofac.Pooling.Test/RegistrationExtensionsNullGuardTests.cs @@ -0,0 +1,164 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using Autofac.Builder; +using Autofac.Pooling.Tests.Common; +using Microsoft.Extensions.ObjectPool; +using Xunit; + +namespace Autofac.Pooling.Test; + +/// +/// Verifies that every public registration overload null-guards its parameters. +/// +public class RegistrationExtensionsNullGuardTests +{ + private static IRegistrationBuilder NullRegistration + => null!; + + [Fact] + public void PerLifetimeScope_NullRegistrationThrows() + { + Assert.Throws(() => + NullRegistration.PooledInstancePerLifetimeScope()); + } + + [Fact] + public void PerLifetimeScope_MaximumRetained_NullRegistrationThrows() + { + Assert.Throws(() => + NullRegistration.PooledInstancePerLifetimeScope(8)); + } + + [Fact] + public void PerLifetimeScope_Policy_NullRegistrationThrows() + { + Assert.Throws(() => + NullRegistration.PooledInstancePerLifetimeScope(new DefaultPooledRegistrationPolicy())); + } + + [Fact] + public void PerLifetimeScope_PolicyFactory_NullRegistrationThrows() + { + Assert.Throws(() => + NullRegistration.PooledInstancePerLifetimeScope( + ctx => new DefaultPooledRegistrationPolicy())); + } + + [Fact] + public void PerLifetimeScope_ProviderFactory_NullRegistrationThrows() + { + Assert.Throws(() => + NullRegistration.PooledInstancePerLifetimeScope( + ctx => new DefaultObjectPoolProvider())); + } + + [Fact] + public void PerLifetimeScope_PolicyAndProviderFactory_NullRegistrationThrows() + { + Assert.Throws(() => + NullRegistration.PooledInstancePerLifetimeScope( + ctx => new DefaultPooledRegistrationPolicy(), + ctx => new DefaultObjectPoolProvider())); + } + + [Fact] + public void PerMatchingLifetimeScope_NullRegistrationThrows() + { + Assert.Throws(() => + NullRegistration.PooledInstancePerMatchingLifetimeScope("tag")); + } + + [Fact] + public void PerMatchingLifetimeScope_MaximumRetained_NullRegistrationThrows() + { + Assert.Throws(() => + NullRegistration.PooledInstancePerMatchingLifetimeScope(8, "tag")); + } + + [Fact] + public void PerMatchingLifetimeScope_Policy_NullRegistrationThrows() + { + Assert.Throws(() => + NullRegistration.PooledInstancePerMatchingLifetimeScope( + new DefaultPooledRegistrationPolicy(), "tag")); + } + + [Fact] + public void PerMatchingLifetimeScope_PolicyFactory_NullRegistrationThrows() + { + Assert.Throws(() => + NullRegistration.PooledInstancePerMatchingLifetimeScope( + ctx => new DefaultPooledRegistrationPolicy(), "tag")); + } + + [Fact] + public void PerMatchingLifetimeScope_ProviderFactory_NullRegistrationThrows() + { + Assert.Throws(() => + NullRegistration.PooledInstancePerMatchingLifetimeScope( + ctx => new DefaultObjectPoolProvider(), "tag")); + } + + [Fact] + public void PerMatchingLifetimeScope_PolicyAndProviderFactory_NullRegistrationThrows() + { + Assert.Throws(() => + NullRegistration.PooledInstancePerMatchingLifetimeScope( + ctx => new DefaultPooledRegistrationPolicy(), + ctx => new DefaultObjectPoolProvider(), + "tag")); + } + + [Fact] + public void PerLifetimeScope_NullPolicyThrows() + { + var reg = new ContainerBuilder() + .RegisterType() + .As(); + + Assert.Throws(() => + reg.PooledInstancePerLifetimeScope((IPooledRegistrationPolicy)null!)); + } + + [Fact] + public void PerMatchingLifetimeScope_NullPolicyThrows() + { + var reg = new ContainerBuilder() + .RegisterType() + .As(); + + Assert.Throws(() => + reg.PooledInstancePerMatchingLifetimeScope( + (IPooledRegistrationPolicy)null!, "tag")); + } + + [Fact] + public void PerMatchingLifetimeScope_PolicyAndProviderFactory_NullPolicyFactoryThrows() + { + var reg = new ContainerBuilder() + .RegisterType() + .As(); + + Assert.Throws(() => + reg.PooledInstancePerMatchingLifetimeScope( + (Func>)null!, + ctx => new DefaultObjectPoolProvider(), + "tag")); + } + + [Fact] + public void PerMatchingLifetimeScope_PolicyAndProviderFactory_NullProviderFactoryThrows() + { + var reg = new ContainerBuilder() + .RegisterType() + .As(); + + Assert.Throws(() => + reg.PooledInstancePerMatchingLifetimeScope( + ctx => new DefaultPooledRegistrationPolicy(), + (Func)null!, + "tag")); + } +} diff --git a/test/Autofac.Pooling.Test/RegistrationExtensionsTests.cs b/test/Autofac.Pooling.Test/RegistrationExtensionsTests.cs index b3b7025..5407d86 100644 --- a/test/Autofac.Pooling.Test/RegistrationExtensionsTests.cs +++ b/test/Autofac.Pooling.Test/RegistrationExtensionsTests.cs @@ -16,6 +16,87 @@ public void RequiresCallbackContainer() Assert.Throws(() => regBuilder.PooledInstancePerLifetimeScope()); } + [Fact] + public void MatchingScopeWithMaximumRetainedRegistersAndResolves() + { + var builder = new ContainerBuilder(); + + builder.RegisterType() + .As() + .PooledInstancePerMatchingLifetimeScope(4, "tag"); + + var container = builder.Build(); + + IPooledService instance; + + using (var scope = container.BeginLifetimeScope("tag")) + { + instance = scope.Resolve(); + Assert.NotNull(instance); + Assert.Equal(1, instance.GetCalled); + } + + // Returned to the pool when the matching scope ends. + Assert.Equal(1, instance.ReturnCalled); + + container.Dispose(); + } + + [Fact] + public void LifetimeOverriddenAfterPoolingSkipsPoolRegistrations() + { + // Changing the lifetime after PooledInstancePerLifetimeScope means the + // deferred callback should fall back to the original behavior and not + // wire up the pool. The component then behaves as a plain singleton. + var builder = new ContainerBuilder(); + + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope() + .SingleInstance(); + + var container = builder.Build(); + + var first = container.Resolve(); + var second = container.Resolve(); + + // SingleInstance won, so it is the same instance and the pool's + // get-from-pool hook never fired. + Assert.Same(first, second); + Assert.Equal(0, first.GetCalled); + + container.Dispose(); + } + + [Fact] + public void DistinctPooledTypesGetDistinctPools() + { + // Exercises PoolService equality/hash-code: two pooled registrations + // must resolve to independent pools rather than sharing one. + var builder = new ContainerBuilder(); + + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(); + + builder.RegisterType() + .AsSelf() + .PooledInstancePerLifetimeScope(); + + var container = builder.Build(); + + using (var scope = container.BeginLifetimeScope()) + { + var first = scope.Resolve(); + var other = scope.Resolve(); + + Assert.IsType(first); + Assert.NotNull(other); + } + + container.Dispose(); + } + [Fact] [SuppressMessage("CA2000", "CA2000", Justification = "The container will dispose of the object.")] public void NoProvidedInstances() @@ -38,4 +119,31 @@ public void OnReleaseNotCompatible() Assert.Throws(() => builder.Build()); } + + [Fact] + public void OtherLifecycleEventsAreCompatible() + { + // A non-OnRelease lifecycle event adds a CoreEventMiddleware of a + // different event type; pooling must allow it (only OnRelease is + // rejected). + var builder = new ContainerBuilder(); + + var activated = false; + + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope() + .OnActivated(args => activated = true); + + var container = builder.Build(); + + using (var scope = container.BeginLifetimeScope()) + { + scope.Resolve(); + } + + Assert.True(activated); + + container.Dispose(); + } } From 3681b22f0a8a7e50d3c6ec623f28298ddd678767 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Wed, 17 Jun 2026 08:18:45 -0700 Subject: [PATCH 2/8] Improved example in the README. --- README.md | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 396dc08..8624829 100644 --- a/README.md +++ b/README.md @@ -56,28 +56,48 @@ By default, pooled instances are stored in a `DefaultObjectPool` sized from the ```csharp var builder = new ContainerBuilder(); +// Register the backing cache in Autofac. Because it is registered here, the +// container owns it and disposes it at shutdown, and it can be shared with the +// rest of the application. +builder.RegisterInstance(new MemoryCache(new MemoryCacheOptions())) + .As(); + +// Register the provider itself so Autofac constructs it and injects the cache. +builder.RegisterType() + .SingleInstance(); + builder.RegisterType() .As() - .PooledInstancePerLifetimeScope(ctx => new CacheObjectPoolProvider()); + // The provider factory receives the IComponentContext, so it can resolve + // the provider - and its dependencies - straight from the container. + .PooledInstancePerLifetimeScope( + ctx => ctx.Resolve()); var container = builder.Build(); ``` Autofac still owns *construction* of the pooled instances (they are resolved through the container, so dependency injection and the `IPooledComponent` / `IPooledRegistrationPolicy` callbacks all work as normal). The provider only controls storage and eviction. The provider's `Create(IPooledObjectPolicy)` method is the seam: Autofac hands in its own policy whose `Create()` resolves a fully-injected instance, so your pool delegates construction to it rather than new-ing up objects itself. -A minimal cache-backed provider looks like this: +A cache-backed provider that takes its cache from Autofac looks like this: ```csharp public sealed class CacheObjectPoolProvider : ObjectPoolProvider { - // One shared cache backs every pool this provider creates. - private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions()); + private readonly IMemoryCache _cache; + + // The cache is injected from Autofac rather than created here, so its + // lifetime is managed by the container and shared across every pool this + // provider creates. + public CacheObjectPoolProvider(IMemoryCache cache) + { + _cache = cache; + } public override ObjectPool Create(IPooledObjectPolicy policy) => new CacheObjectPool(_cache, policy); } -public sealed class CacheObjectPool : ObjectPool, IDisposable +public sealed class CacheObjectPool : ObjectPool where T : class { private readonly IMemoryCache _cache; @@ -100,7 +120,11 @@ public sealed class CacheObjectPool : ObjectPool, IDisposable // The policy decides whether the instance is fit to be retained. if (_policy.Return(obj)) { - _cache.Set(typeof(T), obj, /* eviction options */ TimeSpan.FromMinutes(5)); + // Dispose the instance when the cache eventually evicts it; the pool + // owns disposal of instances the cache drops. + var options = new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromMinutes(5) }; + options.RegisterPostEvictionCallback((_, value, _, _) => (value as IDisposable)?.Dispose()); + _cache.Set(typeof(T), obj, options); } else if (obj is IDisposable disposable) { @@ -108,14 +132,14 @@ public sealed class CacheObjectPool : ObjectPool, IDisposable disposable.Dispose(); } } - - public void Dispose() => _cache.Dispose(); } ``` +The pool does not implement `IDisposable` because it does not own the cache — Autofac creates the `IMemoryCache`, so Autofac disposes it. The pool only takes responsibility for the pooled instances themselves, disposing them when the policy declines a return and when the cache evicts them. + Things to know about custom providers: -- The provider factory is invoked **once** per registration, when the pool is built, resolved from the pool-owning (root) scope. You can resolve the provider as a singleton component and share it across several registrations (the cache-backed example above shares one cache for every type it pools). +- The provider factory is invoked **once** per registration, when the pool is built, resolved from the pool-owning (root) scope. Because the factory gets an `IComponentContext`, the provider can be registered like any other component and resolved from the container, letting Autofac inject its dependencies (the `IMemoryCache` above). Registering the provider and cache as singletons shares one provider and cache across every type that uses them. - With a custom provider, `IPooledRegistrationPolicy.MaximumRetained` does **not** size the pool — the provider owns sizing and eviction. You can still supply a custom policy alongside the provider with the `PooledInstancePerLifetimeScope(policyFactory, providerFactory)` overload to control the `Get` / `Return` behavior. - The pool is **shared across all lifetime scopes and threads**, so your pool must be thread-safe. - **Disposal contract:** From 7fed44d1d54b64816f7cb65dede3bcd707f300e1 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Wed, 17 Jun 2026 08:30:18 -0700 Subject: [PATCH 3/8] Add example showing memory cache pool. --- .../Autofac.Pooling.Test.csproj | 1 + .../Caching/CacheBackedPoolExampleTests.cs | 91 ++++++++++++++ .../Examples/Caching/CacheObjectPool.cs | 84 +++++++++++++ .../Caching/CacheObjectPoolProvider.cs | 54 +++++++++ .../Examples/Caching/ExampleComponents.cs | 112 ++++++++++++++++++ 5 files changed, 342 insertions(+) create mode 100644 test/Autofac.Pooling.Test/Examples/Caching/CacheBackedPoolExampleTests.cs create mode 100644 test/Autofac.Pooling.Test/Examples/Caching/CacheObjectPool.cs create mode 100644 test/Autofac.Pooling.Test/Examples/Caching/CacheObjectPoolProvider.cs create mode 100644 test/Autofac.Pooling.Test/Examples/Caching/ExampleComponents.cs diff --git a/test/Autofac.Pooling.Test/Autofac.Pooling.Test.csproj b/test/Autofac.Pooling.Test/Autofac.Pooling.Test.csproj index 74c7d0c..2a4b85f 100644 --- a/test/Autofac.Pooling.Test/Autofac.Pooling.Test.csproj +++ b/test/Autofac.Pooling.Test/Autofac.Pooling.Test.csproj @@ -16,6 +16,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/test/Autofac.Pooling.Test/Examples/Caching/CacheBackedPoolExampleTests.cs b/test/Autofac.Pooling.Test/Examples/Caching/CacheBackedPoolExampleTests.cs new file mode 100644 index 0000000..3f800ae --- /dev/null +++ b/test/Autofac.Pooling.Test/Examples/Caching/CacheBackedPoolExampleTests.cs @@ -0,0 +1,91 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Microsoft.Extensions.Caching.Memory; +using Xunit; + +namespace Autofac.Pooling.Test.Examples.Caching; + +/// +/// End-to-end example showing how to back a pooled registration with a custom +/// that draws +/// its dependencies (an ) from Autofac. Mirrors the +/// "custom pool provider" sample in the README. +/// +public class CacheBackedPoolExampleTests +{ + [SuppressMessage("CA2000", "CA2000", Justification = "The cache is owned and disposed by the container.")] + private static IContainer BuildContainer() + { + var builder = new ContainerBuilder(); + + // Register the backing cache in Autofac. Because it is registered here, + // the container owns it and disposes it at shutdown, and it can be shared + // with the rest of the application. + builder.RegisterInstance(new MemoryCache(new MemoryCacheOptions())) + .As(); + + // Register the provider itself so Autofac constructs it and injects the + // cache. + builder.RegisterType() + .SingleInstance(); + + // Pool the connection, using the cache-backed provider resolved from the + // container. The provider factory receives the IComponentContext, so it + // can resolve the provider - and its dependencies - straight from + // Autofac. + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(ctx => ctx.Resolve()); + + // A plain consumer that receives a pooled connection by injection. + builder.RegisterType(); + + return builder.Build(); + } + + [Fact] + public void ConsumerReceivesAPooledConnection() + { + using var container = BuildContainer(); + + using var scope = container.BeginLifetimeScope(); + + // The consumer is injected with a pooled connection like any other + // dependency; nothing about the registration leaks into the consumer. + var consumer = scope.Resolve(); + + Assert.NotNull(consumer.Connection); + Assert.Equal("connection-" + consumer.Connection.InstanceId, consumer.Connection.DoSomething()); + + // The instance came out of the pool, so the get-from-pool hook fired. + Assert.Equal(1, consumer.Connection.GetFromPoolCount); + } + + [Fact] + public void PooledInstanceIsReusedAcrossScopes() + { + using var container = BuildContainer(); + + int firstInstanceId; + + // First scope: a connection is created and, when the scope ends, returned + // to the cache-backed pool. + using (var scope = container.BeginLifetimeScope()) + { + var connection = scope.Resolve(); + firstInstanceId = connection.InstanceId; + Assert.Equal(1, connection.GetFromPoolCount); + } + + // Second scope: the same instance is fetched back from the cache rather + // than a new one being constructed, and the get-from-pool hook fires + // again on the reused instance. + using (var scope = container.BeginLifetimeScope()) + { + var connection = scope.Resolve(); + Assert.Equal(firstInstanceId, connection.InstanceId); + Assert.Equal(2, connection.GetFromPoolCount); + } + } +} diff --git a/test/Autofac.Pooling.Test/Examples/Caching/CacheObjectPool.cs b/test/Autofac.Pooling.Test/Examples/Caching/CacheObjectPool.cs new file mode 100644 index 0000000..73902fa --- /dev/null +++ b/test/Autofac.Pooling.Test/Examples/Caching/CacheObjectPool.cs @@ -0,0 +1,84 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#nullable enable + +using System; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.ObjectPool; + +namespace Autofac.Pooling.Test.Examples.Caching; + +/// +/// An that stores a single pooled instance of +/// in a shared , letting the +/// cache's expiration policy evict idle instances. +/// +/// The type of object being pooled. +/// +/// +/// This pool does not implement because it does not +/// own the cache - Autofac created the , so Autofac +/// disposes it. The pool only takes responsibility for the pooled instances +/// themselves: it disposes an instance when the policy declines a return, and +/// registers an eviction callback so the instance is disposed when the cache +/// drops it. +/// +/// +/// The cache is shared across all lifetime scopes and threads, so this pool must +/// be safe for concurrent use; is thread-safe. +/// +/// +public sealed class CacheObjectPool : ObjectPool + where T : class +{ + private readonly IMemoryCache _cache; + private readonly IPooledObjectPolicy _policy; + + /// + /// Initializes a new instance of the class. + /// + /// The shared cache used to store pooled instances. + /// + /// The policy used to create new instances and to decide whether a returned + /// instance should be retained. + /// + public CacheObjectPool(IMemoryCache cache, IPooledObjectPolicy policy) + { + _cache = cache; + _policy = policy; + } + + /// + /// Gets an instance from the cache, or builds a new one through the policy + /// (and therefore through Autofac) on a cache miss. + /// + /// An instance of . + public override T Get() + => _cache.TryGetValue(typeof(T), out T? item) && item is not null + ? item + : _policy.Create(); + + /// + /// Returns an instance to the pool, storing it in the cache when the policy + /// accepts it and disposing it otherwise. + /// + /// The instance being returned. + public override void Return(T obj) + { + // The policy decides whether the instance is fit to be retained. + if (_policy.Return(obj)) + { + // Dispose the instance when the cache eventually evicts it; the pool + // owns disposal of instances the cache drops. + var options = new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromMinutes(5) }; + options.RegisterPostEvictionCallback((_, value, _, _) => (value as IDisposable)?.Dispose()); + _cache.Set(typeof(T), obj, options); + } + else if (obj is IDisposable disposable) + { + // The pool owns disposal of instances it declines. + disposable.Dispose(); + } + } +} diff --git a/test/Autofac.Pooling.Test/Examples/Caching/CacheObjectPoolProvider.cs b/test/Autofac.Pooling.Test/Examples/Caching/CacheObjectPoolProvider.cs new file mode 100644 index 0000000..33d8810 --- /dev/null +++ b/test/Autofac.Pooling.Test/Examples/Caching/CacheObjectPoolProvider.cs @@ -0,0 +1,54 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.ObjectPool; + +namespace Autofac.Pooling.Test.Examples.Caching; + +/// +/// An that backs each pool it creates with a +/// shared , demonstrating how a custom provider can +/// take its dependencies from Autofac. +/// +/// +/// Register this provider as a component so that Autofac constructs it and +/// injects the cache, then point a pooled registration at it with +/// ctx => ctx.Resolve<CacheObjectPoolProvider>(). The provider only +/// controls where instances are stored and when they are evicted; Autofac still +/// owns construction of the pooled instances through the policy it supplies to +/// . +/// +public sealed class CacheObjectPoolProvider : ObjectPoolProvider +{ + private readonly IMemoryCache _cache; + + /// + /// Initializes a new instance of the + /// class. + /// + /// + /// The cache used to store pooled instances. Because the cache is injected + /// rather than created here, its lifetime is owned by the container and the + /// same cache is shared across every pool this provider creates. + /// + public CacheObjectPoolProvider(IMemoryCache cache) + { + _cache = cache; + } + + /// + /// Creates a pool that stores instances of in the + /// shared cache. + /// + /// The type of object to pool. + /// + /// The policy supplied by Autofac. Its + /// resolves a fully injected instance through the container, and its + /// decides whether an instance is + /// fit to be retained. + /// + /// A cache-backed pool for . + public override ObjectPool Create(IPooledObjectPolicy policy) + => new CacheObjectPool(_cache, policy); +} diff --git a/test/Autofac.Pooling.Test/Examples/Caching/ExampleComponents.cs b/test/Autofac.Pooling.Test/Examples/Caching/ExampleComponents.cs new file mode 100644 index 0000000..5a8bf4f --- /dev/null +++ b/test/Autofac.Pooling.Test/Examples/Caching/ExampleComponents.cs @@ -0,0 +1,112 @@ +// 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.Collections.Generic; +using System.Threading; +using Autofac.Core; + +namespace Autofac.Pooling.Test.Examples.Caching; + +/// +/// A connection-like service that is expensive enough to be worth pooling. +/// +public interface ICustomConnection +{ + /// + /// Gets a stable identifier for the underlying instance, used by the tests + /// to tell whether the same pooled instance was handed out again. + /// + int InstanceId + { + get; + } + + /// + /// Gets the number of times this instance has been taken from the pool. + /// + int GetFromPoolCount + { + get; + } + + /// + /// Does some representative work. + /// + /// A result derived from the work. + string DoSomething(); +} + +/// +/// A pooled implementation of that records its +/// pool lifecycle so the tests can observe reuse. +/// +/// +/// Implementing lets the component react when it +/// is taken from or returned to the pool - the natural place to reset +/// per-use state on a reused instance. +/// +public sealed class MyCustomConnection : ICustomConnection, IPooledComponent +{ + private static int _instanceCounter; + + /// + /// Initializes a new instance of the class. + /// + public MyCustomConnection() + { + InstanceId = Interlocked.Increment(ref _instanceCounter); + } + + /// + public int InstanceId + { + get; + } + + /// + public int GetFromPoolCount + { + get; private set; + } + + /// + public string DoSomething() => $"connection-{InstanceId}"; + + /// + public void OnGetFromPool(IComponentContext context, IEnumerable parameters) + { + GetFromPoolCount++; + } + + /// + public void OnReturnToPool() + { + // A real connection would reset per-use state here (clear buffers, + // roll back transactions, and so on) before being reused. + } +} + +/// +/// A component that depends on , used to show that +/// a pooled instance is injected into a consumer like any other dependency. +/// +public sealed class ConnectionConsumer +{ + /// + /// Initializes a new instance of the class. + /// + /// The pooled connection injected by Autofac. + public ConnectionConsumer(ICustomConnection connection) + { + Connection = connection; + } + + /// + /// Gets the pooled connection that was injected. + /// + public ICustomConnection Connection + { + get; + } +} From 3e7f55ebe14fe09fd9314e16ec8c40638ece8b09 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Wed, 17 Jun 2026 09:06:38 -0700 Subject: [PATCH 4/8] Standardize projects and resolve analyzer warnings. --- src/Autofac.Pooling/Autofac.Pooling.csproj | 88 ++++++-------- .../DefaultPooledRegistrationPolicy.cs | 2 - src/Autofac.Pooling/IPooledComponent.cs | 2 - .../IPooledRegistrationPolicy.cs | 2 - src/Autofac.Pooling/PoolActivator.cs | 1 - src/Autofac.Pooling/PoolGetActivator.cs | 1 - .../PoolGetActivatorResources.Designer.cs | 72 ----------- src/Autofac.Pooling/PoolService.cs | 1 - .../PoolServiceResources.Designer.cs | 72 ----------- src/Autofac.Pooling/PooledInstanceContext.cs | 1 - src/Autofac.Pooling/PooledInstanceTracker.cs | 1 - .../PooledInstanceUnpackMiddleware.cs | 1 - .../Properties/AssemblyInfo.cs | 5 - src/Autofac.Pooling/RegistrationExtensions.cs | 3 - ...egistrationExtensionsResources.Designer.cs | 90 -------------- .../Autofac.Pooling.Test.csproj | 41 ++++--- .../Autofac.Pooling.Test/Common/Dependency.cs | 5 +- .../Common/DependentPooledComponent.cs | 4 +- .../Common/IPooledService.cs | 5 +- .../Common/OtherPooledComponent.cs | 5 +- .../Common/PoolTrackingPolicy.cs | 6 +- .../Common/PooledComponent.cs | 5 +- .../Common/TrackingObjectPoolProvider.cs | 5 +- test/Autofac.Pooling.Test/ConcurrencyTests.cs | 13 +- .../CustomProviderTests.cs | 4 - .../DefaultPooledRegistrationPolicyTests.cs | 3 - test/Autofac.Pooling.Test/DisposableTests.cs | 6 +- .../Caching/CacheBackedPoolExampleTests.cs | 1 - .../Examples/Caching/CacheObjectPool.cs | 1 - .../Examples/Caching/ConnectionConsumer.cs | 28 +++++ .../Examples/Caching/ExampleComponents.cs | 112 ------------------ .../Examples/Caching/ICustomConnection.cs | 35 ++++++ .../Examples/Caching/MyCustomConnection.cs | 56 +++++++++ .../FactoryOverloadTests.cs | 4 - .../ImplicitRelationshipTests.cs | 6 +- .../LifetimeScopeTests.cs | 6 +- test/Autofac.Pooling.Test/PolicyTests.cs | 13 +- .../PoolActivatorTests.cs | 2 - test/Autofac.Pooling.Test/PoolServiceTests.cs | 3 - .../PooledComponentTests.cs | 6 +- test/Autofac.Pooling.Test/PoolingTests.cs | 15 ++- .../RegistrationExtensionsNullGuardTests.cs | 2 - .../RegistrationExtensionsTests.cs | 5 +- 43 files changed, 246 insertions(+), 493 deletions(-) delete mode 100644 src/Autofac.Pooling/PoolGetActivatorResources.Designer.cs delete mode 100644 src/Autofac.Pooling/PoolServiceResources.Designer.cs delete mode 100644 src/Autofac.Pooling/RegistrationExtensionsResources.Designer.cs create mode 100644 test/Autofac.Pooling.Test/Examples/Caching/ConnectionConsumer.cs delete mode 100644 test/Autofac.Pooling.Test/Examples/Caching/ExampleComponents.cs create mode 100644 test/Autofac.Pooling.Test/Examples/Caching/ICustomConnection.cs create mode 100644 test/Autofac.Pooling.Test/Examples/Caching/MyCustomConnection.cs diff --git a/src/Autofac.Pooling/Autofac.Pooling.csproj b/src/Autofac.Pooling/Autofac.Pooling.csproj index 7954479..eb4f749 100644 --- a/src/Autofac.Pooling/Autofac.Pooling.csproj +++ b/src/Autofac.Pooling/Autofac.Pooling.csproj @@ -1,91 +1,81 @@  - + + Support for pooled instance lifetime scopes in Autofac dependency injection. + Copyright © 2020 Autofac Contributors + Autofac Contributors + Autofac + Autofac + ../../Autofac.snk + true + en-US + net10.0;net8.0;netstandard2.1;netstandard2.0 - enable latest + enable true - ../../Autofac.snk - true ../../build/Source.ruleset - AllEnabledByDefault true - false - false - false - false - - - - - Support for pooled instance lifetime scopes in Autofac dependency injection. + AllEnabledByDefault + enable + autofac;di;ioc;dependencyinjection;pooling Release notes are at https://github.com/autofac/Autofac.Pooling/releases - https://cloud.githubusercontent.com/assets/1156571/13684110/16b8f152-e6bf-11e5-84ae-22c66c6d351a.png - https://autofac.org icon.png + https://autofac.org MIT - Autofac Contributors - Autofac - Autofac + README.md git https://github.com/autofac/Autofac.Pooling + true true true - $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + true + snupkg + + PrepareResources;$(CompileDependsOn) + - - + + - All - - - All + all all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + - - - True - True - PoolGetActivatorResources.resx - - - True - True - PoolServiceResources.resx - - - True - True - RegistrationExtensionsResources.resx - - + + + MSBuild:Compile + CSharp + $(IntermediateOutputPath)%(Filename).Designer.cs + %(Filename) + + - ResXFileCodeGenerator - PoolGetActivatorResources.Designer.cs + Autofac.Pooling - ResXFileCodeGenerator - PoolServiceResources.Designer.cs + Autofac.Pooling - ResXFileCodeGenerator - RegistrationExtensionsResources.Designer.cs + Autofac.Pooling diff --git a/src/Autofac.Pooling/DefaultPooledRegistrationPolicy.cs b/src/Autofac.Pooling/DefaultPooledRegistrationPolicy.cs index 20c45ee..c0a382d 100644 --- a/src/Autofac.Pooling/DefaultPooledRegistrationPolicy.cs +++ b/src/Autofac.Pooling/DefaultPooledRegistrationPolicy.cs @@ -1,8 +1,6 @@ // 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.Collections.Generic; using Autofac.Core; namespace Autofac.Pooling; diff --git a/src/Autofac.Pooling/IPooledComponent.cs b/src/Autofac.Pooling/IPooledComponent.cs index dbcdcf5..8ed466e 100644 --- a/src/Autofac.Pooling/IPooledComponent.cs +++ b/src/Autofac.Pooling/IPooledComponent.cs @@ -1,8 +1,6 @@ // 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.Collections.Generic; using Autofac.Core; namespace Autofac.Pooling; diff --git a/src/Autofac.Pooling/IPooledRegistrationPolicy.cs b/src/Autofac.Pooling/IPooledRegistrationPolicy.cs index 239f69d..dca3cbf 100644 --- a/src/Autofac.Pooling/IPooledRegistrationPolicy.cs +++ b/src/Autofac.Pooling/IPooledRegistrationPolicy.cs @@ -1,8 +1,6 @@ // 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.Collections.Generic; using Autofac.Core; namespace Autofac.Pooling; diff --git a/src/Autofac.Pooling/PoolActivator.cs b/src/Autofac.Pooling/PoolActivator.cs index f2bffa4..de35c3c 100644 --- a/src/Autofac.Pooling/PoolActivator.cs +++ b/src/Autofac.Pooling/PoolActivator.cs @@ -1,7 +1,6 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System; using Autofac.Core; using Autofac.Core.Resolving.Pipeline; using Microsoft.Extensions.ObjectPool; diff --git a/src/Autofac.Pooling/PoolGetActivator.cs b/src/Autofac.Pooling/PoolGetActivator.cs index aeef397..460f14f 100644 --- a/src/Autofac.Pooling/PoolGetActivator.cs +++ b/src/Autofac.Pooling/PoolGetActivator.cs @@ -1,7 +1,6 @@ // 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.Globalization; using Autofac.Core; using Autofac.Core.Resolving.Pipeline; diff --git a/src/Autofac.Pooling/PoolGetActivatorResources.Designer.cs b/src/Autofac.Pooling/PoolGetActivatorResources.Designer.cs deleted file mode 100644 index b89f0d5..0000000 --- a/src/Autofac.Pooling/PoolGetActivatorResources.Designer.cs +++ /dev/null @@ -1,72 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Autofac.Pooling { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class PoolGetActivatorResources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal PoolGetActivatorResources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Autofac.Pooling.PoolGetActivatorResources", typeof(PoolGetActivatorResources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to The custom pool policy provided by {0} did not return an instance of {1} when requested.. - /// - internal static string PolicyMustReturnInstance { - get { - return ResourceManager.GetString("PolicyMustReturnInstance", resourceCulture); - } - } - } -} diff --git a/src/Autofac.Pooling/PoolService.cs b/src/Autofac.Pooling/PoolService.cs index 1faae61..cf6a115 100644 --- a/src/Autofac.Pooling/PoolService.cs +++ b/src/Autofac.Pooling/PoolService.cs @@ -1,7 +1,6 @@ // 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.Globalization; using Autofac.Core; using Microsoft.Extensions.ObjectPool; diff --git a/src/Autofac.Pooling/PoolServiceResources.Designer.cs b/src/Autofac.Pooling/PoolServiceResources.Designer.cs deleted file mode 100644 index 817abf5..0000000 --- a/src/Autofac.Pooling/PoolServiceResources.Designer.cs +++ /dev/null @@ -1,72 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Autofac.Pooling { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class PoolServiceResources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal PoolServiceResources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Autofac.Pooling.PoolServiceResources", typeof(PoolServiceResources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Pool of {0}. - /// - internal static string Description { - get { - return ResourceManager.GetString("Description", resourceCulture); - } - } - } -} diff --git a/src/Autofac.Pooling/PooledInstanceContext.cs b/src/Autofac.Pooling/PooledInstanceContext.cs index 4588c4b..501e389 100644 --- a/src/Autofac.Pooling/PooledInstanceContext.cs +++ b/src/Autofac.Pooling/PooledInstanceContext.cs @@ -1,7 +1,6 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System; using Microsoft.Extensions.ObjectPool; namespace Autofac.Pooling; diff --git a/src/Autofac.Pooling/PooledInstanceTracker.cs b/src/Autofac.Pooling/PooledInstanceTracker.cs index 0d423fa..20f7bef 100644 --- a/src/Autofac.Pooling/PooledInstanceTracker.cs +++ b/src/Autofac.Pooling/PooledInstanceTracker.cs @@ -1,7 +1,6 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System; using Microsoft.Extensions.ObjectPool; namespace Autofac.Pooling; diff --git a/src/Autofac.Pooling/PooledInstanceUnpackMiddleware.cs b/src/Autofac.Pooling/PooledInstanceUnpackMiddleware.cs index e899610..e010bd4 100644 --- a/src/Autofac.Pooling/PooledInstanceUnpackMiddleware.cs +++ b/src/Autofac.Pooling/PooledInstanceUnpackMiddleware.cs @@ -1,7 +1,6 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System; using Autofac.Core.Resolving.Pipeline; namespace Autofac.Pooling; diff --git a/src/Autofac.Pooling/Properties/AssemblyInfo.cs b/src/Autofac.Pooling/Properties/AssemblyInfo.cs index c66a4e0..23ebb11 100644 --- a/src/Autofac.Pooling/Properties/AssemblyInfo.cs +++ b/src/Autofac.Pooling/Properties/AssemblyInfo.cs @@ -1,14 +1,9 @@ // 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.Reflection; -using System.Resources; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; [assembly: CLSCompliant(false)] [assembly: ComVisible(false)] -[assembly: NeutralResourcesLanguage("en-US")] -[assembly: AssemblyCopyright("Copyright © 2020 Autofac Contributors")] [assembly: InternalsVisibleTo("Autofac.Pooling.Test, PublicKey=00240000048000009400000006020000002400005253413100040000010001008728425885ef385e049261b18878327dfaaf0d666dea3bd2b0e4f18b33929ad4e5fbc9087e7eda3c1291d2de579206d9b4292456abffbe8be6c7060b36da0c33b883e3878eaf7c89fddf29e6e27d24588e81e86f3a22dd7b1a296b5f06fbfb500bbd7410faa7213ef4e2ce7622aefc03169b0324bcd30ccfe9ac8204e4960be6")] diff --git a/src/Autofac.Pooling/RegistrationExtensions.cs b/src/Autofac.Pooling/RegistrationExtensions.cs index 057d062..47294da 100644 --- a/src/Autofac.Pooling/RegistrationExtensions.cs +++ b/src/Autofac.Pooling/RegistrationExtensions.cs @@ -1,9 +1,6 @@ // 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.Collections.Generic; -using System.Linq; using Autofac.Builder; using Autofac.Core; using Autofac.Core.Activators.ProvidedInstance; diff --git a/src/Autofac.Pooling/RegistrationExtensionsResources.Designer.cs b/src/Autofac.Pooling/RegistrationExtensionsResources.Designer.cs deleted file mode 100644 index 5cb17ff..0000000 --- a/src/Autofac.Pooling/RegistrationExtensionsResources.Designer.cs +++ /dev/null @@ -1,90 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Autofac.Pooling { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class RegistrationExtensionsResources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal RegistrationExtensionsResources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Autofac.Pooling.RegistrationExtensionsResources", typeof(RegistrationExtensionsResources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Pooled registrations cannot be used with provided instances. Use a typed registration or a delegate registration instead.. - /// - internal static string CannotUseProvidedInstances { - get { - return ResourceManager.GetString("CannotUseProvidedInstances", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to It is not possible to use custom OnRelease events for pooled registrations. Instead, either provide a custom IPooledRegistrationPolicy to handle items being returned to pool, or implement IPooledComponent on the component's type.. - /// - internal static string OnReleaseNotSupported { - get { - return ResourceManager.GetString("OnReleaseNotSupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You can only specified a pooled registration for registrations that have a callback container attached (e.g., one that was made with a standard ContainerBuilder extension method).. - /// - internal static string RequiresCallbackContainer { - get { - return ResourceManager.GetString("RequiresCallbackContainer", resourceCulture); - } - } - } -} diff --git a/test/Autofac.Pooling.Test/Autofac.Pooling.Test.csproj b/test/Autofac.Pooling.Test/Autofac.Pooling.Test.csproj index 2a4b85f..5d48e46 100644 --- a/test/Autofac.Pooling.Test/Autofac.Pooling.Test.csproj +++ b/test/Autofac.Pooling.Test/Autofac.Pooling.Test.csproj @@ -1,37 +1,48 @@  - net10.0 + net10.0;net8.0 + $(NoWarn);CS1591 + true + ../../Autofac.snk + true + true ../../build/Test.ruleset AllEnabledByDefault true false - ../../Autofac.snk - true + latest + enable + enable + + + + + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - diff --git a/test/Autofac.Pooling.Test/Common/Dependency.cs b/test/Autofac.Pooling.Test/Common/Dependency.cs index b9fefe9..ab3e1bd 100644 --- a/test/Autofac.Pooling.Test/Common/Dependency.cs +++ b/test/Autofac.Pooling.Test/Common/Dependency.cs @@ -1,4 +1,7 @@ -namespace Autofac.Pooling.Tests.Common; +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace Autofac.Pooling.Tests.Common; /// /// A trivial dependency injected into . diff --git a/test/Autofac.Pooling.Test/Common/DependentPooledComponent.cs b/test/Autofac.Pooling.Test/Common/DependentPooledComponent.cs index 11b9f51..af708e1 100644 --- a/test/Autofac.Pooling.Test/Common/DependentPooledComponent.cs +++ b/test/Autofac.Pooling.Test/Common/DependentPooledComponent.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + using Autofac.Core; namespace Autofac.Pooling.Tests.Common; diff --git a/test/Autofac.Pooling.Test/Common/IPooledService.cs b/test/Autofac.Pooling.Test/Common/IPooledService.cs index 951e569..e4aac0c 100644 --- a/test/Autofac.Pooling.Test/Common/IPooledService.cs +++ b/test/Autofac.Pooling.Test/Common/IPooledService.cs @@ -1,4 +1,7 @@ -namespace Autofac.Pooling.Tests.Common; +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace Autofac.Pooling.Tests.Common; public interface IPooledService { diff --git a/test/Autofac.Pooling.Test/Common/OtherPooledComponent.cs b/test/Autofac.Pooling.Test/Common/OtherPooledComponent.cs index edbf163..c048e68 100644 --- a/test/Autofac.Pooling.Test/Common/OtherPooledComponent.cs +++ b/test/Autofac.Pooling.Test/Common/OtherPooledComponent.cs @@ -1,4 +1,7 @@ -namespace Autofac.Pooling.Tests.Common; +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace Autofac.Pooling.Tests.Common; public class OtherPooledComponent : IPooledService { diff --git a/test/Autofac.Pooling.Test/Common/PoolTrackingPolicy.cs b/test/Autofac.Pooling.Test/Common/PoolTrackingPolicy.cs index 7fa7cb3..e04d413 100644 --- a/test/Autofac.Pooling.Test/Common/PoolTrackingPolicy.cs +++ b/test/Autofac.Pooling.Test/Common/PoolTrackingPolicy.cs @@ -1,6 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Threading; +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + using Autofac.Core; namespace Autofac.Pooling.Tests.Common; diff --git a/test/Autofac.Pooling.Test/Common/PooledComponent.cs b/test/Autofac.Pooling.Test/Common/PooledComponent.cs index ffbed02..f2f75c4 100644 --- a/test/Autofac.Pooling.Test/Common/PooledComponent.cs +++ b/test/Autofac.Pooling.Test/Common/PooledComponent.cs @@ -1,5 +1,6 @@ -using System; -using System.Collections.Generic; +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + using Autofac.Core; namespace Autofac.Pooling.Tests.Common; diff --git a/test/Autofac.Pooling.Test/Common/TrackingObjectPoolProvider.cs b/test/Autofac.Pooling.Test/Common/TrackingObjectPoolProvider.cs index 739b225..9ddd083 100644 --- a/test/Autofac.Pooling.Test/Common/TrackingObjectPoolProvider.cs +++ b/test/Autofac.Pooling.Test/Common/TrackingObjectPoolProvider.cs @@ -1,6 +1,7 @@ -using System; +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + using System.Collections.Concurrent; -using System.Threading; using Microsoft.Extensions.ObjectPool; namespace Autofac.Pooling.Tests.Common; diff --git a/test/Autofac.Pooling.Test/ConcurrencyTests.cs b/test/Autofac.Pooling.Test/ConcurrencyTests.cs index a73c2e7..9135101 100644 --- a/test/Autofac.Pooling.Test/ConcurrencyTests.cs +++ b/test/Autofac.Pooling.Test/ConcurrencyTests.cs @@ -1,12 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + using Autofac.Core; using Autofac.Pooling.Tests.Common; -using Xunit; namespace Autofac.Pooling.Test; @@ -66,7 +62,8 @@ private class BlockingPolicy : DefaultPooledRegistrationPolicy, private readonly SemaphoreSlim _semaphore; private bool _disposedValue; - public BlockingPolicy(int maxConcurrentInstances) : base(maxConcurrentInstances) + public BlockingPolicy(int maxConcurrentInstances) + : base(maxConcurrentInstances) { _semaphore = new SemaphoreSlim(maxConcurrentInstances); } diff --git a/test/Autofac.Pooling.Test/CustomProviderTests.cs b/test/Autofac.Pooling.Test/CustomProviderTests.cs index 244a709..5d29a95 100644 --- a/test/Autofac.Pooling.Test/CustomProviderTests.cs +++ b/test/Autofac.Pooling.Test/CustomProviderTests.cs @@ -1,12 +1,8 @@ // 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 Microsoft.Extensions.ObjectPool; -using Xunit; namespace Autofac.Pooling.Test; diff --git a/test/Autofac.Pooling.Test/DefaultPooledRegistrationPolicyTests.cs b/test/Autofac.Pooling.Test/DefaultPooledRegistrationPolicyTests.cs index eddfffb..3d8738c 100644 --- a/test/Autofac.Pooling.Test/DefaultPooledRegistrationPolicyTests.cs +++ b/test/Autofac.Pooling.Test/DefaultPooledRegistrationPolicyTests.cs @@ -1,10 +1,7 @@ // 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 Autofac.Pooling.Tests.Common; -using Xunit; namespace Autofac.Pooling.Test; diff --git a/test/Autofac.Pooling.Test/DisposableTests.cs b/test/Autofac.Pooling.Test/DisposableTests.cs index d903072..b32cb63 100644 --- a/test/Autofac.Pooling.Test/DisposableTests.cs +++ b/test/Autofac.Pooling.Test/DisposableTests.cs @@ -1,6 +1,8 @@ -using Autofac.Features.OwnedInstances; +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Autofac.Features.OwnedInstances; using Autofac.Pooling.Tests.Common; -using Xunit; namespace Autofac.Pooling.Test; diff --git a/test/Autofac.Pooling.Test/Examples/Caching/CacheBackedPoolExampleTests.cs b/test/Autofac.Pooling.Test/Examples/Caching/CacheBackedPoolExampleTests.cs index 3f800ae..bbc7600 100644 --- a/test/Autofac.Pooling.Test/Examples/Caching/CacheBackedPoolExampleTests.cs +++ b/test/Autofac.Pooling.Test/Examples/Caching/CacheBackedPoolExampleTests.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using Microsoft.Extensions.Caching.Memory; -using Xunit; namespace Autofac.Pooling.Test.Examples.Caching; diff --git a/test/Autofac.Pooling.Test/Examples/Caching/CacheObjectPool.cs b/test/Autofac.Pooling.Test/Examples/Caching/CacheObjectPool.cs index 73902fa..cb21988 100644 --- a/test/Autofac.Pooling.Test/Examples/Caching/CacheObjectPool.cs +++ b/test/Autofac.Pooling.Test/Examples/Caching/CacheObjectPool.cs @@ -3,7 +3,6 @@ #nullable enable -using System; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.ObjectPool; diff --git a/test/Autofac.Pooling.Test/Examples/Caching/ConnectionConsumer.cs b/test/Autofac.Pooling.Test/Examples/Caching/ConnectionConsumer.cs new file mode 100644 index 0000000..5a9ccd7 --- /dev/null +++ b/test/Autofac.Pooling.Test/Examples/Caching/ConnectionConsumer.cs @@ -0,0 +1,28 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace Autofac.Pooling.Test.Examples.Caching; + +/// +/// A component that depends on , used to show that +/// a pooled instance is injected into a consumer like any other dependency. +/// +public sealed class ConnectionConsumer +{ + /// + /// Initializes a new instance of the class. + /// + /// The pooled connection injected by Autofac. + public ConnectionConsumer(ICustomConnection connection) + { + Connection = connection; + } + + /// + /// Gets the pooled connection that was injected. + /// + public ICustomConnection Connection + { + get; + } +} diff --git a/test/Autofac.Pooling.Test/Examples/Caching/ExampleComponents.cs b/test/Autofac.Pooling.Test/Examples/Caching/ExampleComponents.cs deleted file mode 100644 index 5a8bf4f..0000000 --- a/test/Autofac.Pooling.Test/Examples/Caching/ExampleComponents.cs +++ /dev/null @@ -1,112 +0,0 @@ -// 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.Collections.Generic; -using System.Threading; -using Autofac.Core; - -namespace Autofac.Pooling.Test.Examples.Caching; - -/// -/// A connection-like service that is expensive enough to be worth pooling. -/// -public interface ICustomConnection -{ - /// - /// Gets a stable identifier for the underlying instance, used by the tests - /// to tell whether the same pooled instance was handed out again. - /// - int InstanceId - { - get; - } - - /// - /// Gets the number of times this instance has been taken from the pool. - /// - int GetFromPoolCount - { - get; - } - - /// - /// Does some representative work. - /// - /// A result derived from the work. - string DoSomething(); -} - -/// -/// A pooled implementation of that records its -/// pool lifecycle so the tests can observe reuse. -/// -/// -/// Implementing lets the component react when it -/// is taken from or returned to the pool - the natural place to reset -/// per-use state on a reused instance. -/// -public sealed class MyCustomConnection : ICustomConnection, IPooledComponent -{ - private static int _instanceCounter; - - /// - /// Initializes a new instance of the class. - /// - public MyCustomConnection() - { - InstanceId = Interlocked.Increment(ref _instanceCounter); - } - - /// - public int InstanceId - { - get; - } - - /// - public int GetFromPoolCount - { - get; private set; - } - - /// - public string DoSomething() => $"connection-{InstanceId}"; - - /// - public void OnGetFromPool(IComponentContext context, IEnumerable parameters) - { - GetFromPoolCount++; - } - - /// - public void OnReturnToPool() - { - // A real connection would reset per-use state here (clear buffers, - // roll back transactions, and so on) before being reused. - } -} - -/// -/// A component that depends on , used to show that -/// a pooled instance is injected into a consumer like any other dependency. -/// -public sealed class ConnectionConsumer -{ - /// - /// Initializes a new instance of the class. - /// - /// The pooled connection injected by Autofac. - public ConnectionConsumer(ICustomConnection connection) - { - Connection = connection; - } - - /// - /// Gets the pooled connection that was injected. - /// - public ICustomConnection Connection - { - get; - } -} diff --git a/test/Autofac.Pooling.Test/Examples/Caching/ICustomConnection.cs b/test/Autofac.Pooling.Test/Examples/Caching/ICustomConnection.cs new file mode 100644 index 0000000..02f41fc --- /dev/null +++ b/test/Autofac.Pooling.Test/Examples/Caching/ICustomConnection.cs @@ -0,0 +1,35 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Autofac.Core; + +namespace Autofac.Pooling.Test.Examples.Caching; + +/// +/// A connection-like service that is expensive enough to be worth pooling. +/// +public interface ICustomConnection +{ + /// + /// Gets a stable identifier for the underlying instance, used by the tests + /// to tell whether the same pooled instance was handed out again. + /// + int InstanceId + { + get; + } + + /// + /// Gets the number of times this instance has been taken from the pool. + /// + int GetFromPoolCount + { + get; + } + + /// + /// Does some representative work. + /// + /// A result derived from the work. + string DoSomething(); +} diff --git a/test/Autofac.Pooling.Test/Examples/Caching/MyCustomConnection.cs b/test/Autofac.Pooling.Test/Examples/Caching/MyCustomConnection.cs new file mode 100644 index 0000000..a5b75c5 --- /dev/null +++ b/test/Autofac.Pooling.Test/Examples/Caching/MyCustomConnection.cs @@ -0,0 +1,56 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Autofac.Core; + +namespace Autofac.Pooling.Test.Examples.Caching; + +/// +/// A pooled implementation of that records its +/// pool lifecycle so the tests can observe reuse. +/// +/// +/// Implementing lets the component react when it +/// is taken from or returned to the pool - the natural place to reset +/// per-use state on a reused instance. +/// +public sealed class MyCustomConnection : ICustomConnection, IPooledComponent +{ + private static int _instanceCounter; + + /// + /// Initializes a new instance of the class. + /// + public MyCustomConnection() + { + InstanceId = Interlocked.Increment(ref _instanceCounter); + } + + /// + public int InstanceId + { + get; + } + + /// + public int GetFromPoolCount + { + get; private set; + } + + /// + public string DoSomething() => $"connection-{InstanceId}"; + + /// + public void OnGetFromPool(IComponentContext context, IEnumerable parameters) + { + GetFromPoolCount++; + } + + /// + public void OnReturnToPool() + { + // A real connection would reset per-use state here (clear buffers, + // roll back transactions, and so on) before being reused. + } +} diff --git a/test/Autofac.Pooling.Test/FactoryOverloadTests.cs b/test/Autofac.Pooling.Test/FactoryOverloadTests.cs index 0573e97..51f5e3d 100644 --- a/test/Autofac.Pooling.Test/FactoryOverloadTests.cs +++ b/test/Autofac.Pooling.Test/FactoryOverloadTests.cs @@ -1,11 +1,7 @@ // 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; diff --git a/test/Autofac.Pooling.Test/ImplicitRelationshipTests.cs b/test/Autofac.Pooling.Test/ImplicitRelationshipTests.cs index 29b2f97..7c2f1f8 100644 --- a/test/Autofac.Pooling.Test/ImplicitRelationshipTests.cs +++ b/test/Autofac.Pooling.Test/ImplicitRelationshipTests.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections.Generic; +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + using Autofac.Features.Metadata; using Autofac.Features.OwnedInstances; using Autofac.Pooling.Tests.Common; -using Xunit; namespace Autofac.Pooling.Test; diff --git a/test/Autofac.Pooling.Test/LifetimeScopeTests.cs b/test/Autofac.Pooling.Test/LifetimeScopeTests.cs index 2a2cc28..a5fc6c3 100644 --- a/test/Autofac.Pooling.Test/LifetimeScopeTests.cs +++ b/test/Autofac.Pooling.Test/LifetimeScopeTests.cs @@ -1,6 +1,8 @@ -using Autofac.Core; +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Autofac.Core; using Autofac.Pooling.Tests.Common; -using Xunit; namespace Autofac.Pooling.Test; diff --git a/test/Autofac.Pooling.Test/PolicyTests.cs b/test/Autofac.Pooling.Test/PolicyTests.cs index b3af42d..f2ebec3 100644 --- a/test/Autofac.Pooling.Test/PolicyTests.cs +++ b/test/Autofac.Pooling.Test/PolicyTests.cs @@ -1,9 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + using Autofac.Core; using Autofac.Pooling.Tests.Common; -using Xunit; namespace Autofac.Pooling.Test; @@ -130,7 +129,6 @@ public void PolicyCanSeeParametersFromThePooledServiceResolve() var policy = new CustomPolicy( (ctxt, param, getCallback) => { - policyReceivedParameters.AddRange(param); return getCallback(); @@ -146,10 +144,11 @@ public void PolicyCanSeeParametersFromThePooledServiceResolve() using (var scope = container.BeginLifetimeScope()) { - var _ = scope.Resolve(new NamedParameter("Val1", 123), new TypedParameter(typeof(int), 456)); + _ = scope.Resolve(new NamedParameter("Val1", 123), new TypedParameter(typeof(int), 456)); } - Assert.Collection(policyReceivedParameters, + Assert.Collection( + policyReceivedParameters, p => { Assert.Equal("Val1", (p as NamedParameter)?.Name); diff --git a/test/Autofac.Pooling.Test/PoolActivatorTests.cs b/test/Autofac.Pooling.Test/PoolActivatorTests.cs index 3ca7b36..83f41fc 100644 --- a/test/Autofac.Pooling.Test/PoolActivatorTests.cs +++ b/test/Autofac.Pooling.Test/PoolActivatorTests.cs @@ -1,11 +1,9 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System; using Autofac.Core; using Autofac.Pooling.Tests.Common; using Microsoft.Extensions.ObjectPool; -using Xunit; namespace Autofac.Pooling.Test; diff --git a/test/Autofac.Pooling.Test/PoolServiceTests.cs b/test/Autofac.Pooling.Test/PoolServiceTests.cs index 5475f73..581ef4a 100644 --- a/test/Autofac.Pooling.Test/PoolServiceTests.cs +++ b/test/Autofac.Pooling.Test/PoolServiceTests.cs @@ -1,11 +1,8 @@ // 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 Autofac.Core; using Autofac.Pooling.Tests.Common; -using Xunit; namespace Autofac.Pooling.Test; diff --git a/test/Autofac.Pooling.Test/PooledComponentTests.cs b/test/Autofac.Pooling.Test/PooledComponentTests.cs index 08fb98c..160a4d4 100644 --- a/test/Autofac.Pooling.Test/PooledComponentTests.cs +++ b/test/Autofac.Pooling.Test/PooledComponentTests.cs @@ -1,6 +1,8 @@ -using Autofac.Features.OwnedInstances; +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Autofac.Features.OwnedInstances; using Autofac.Pooling.Tests.Common; -using Xunit; namespace Autofac.Pooling.Test; diff --git a/test/Autofac.Pooling.Test/PoolingTests.cs b/test/Autofac.Pooling.Test/PoolingTests.cs index 26a63a5..0a1cd8c 100644 --- a/test/Autofac.Pooling.Test/PoolingTests.cs +++ b/test/Autofac.Pooling.Test/PoolingTests.cs @@ -1,9 +1,11 @@ -using Autofac.Pooling.Tests.Common; -using Xunit; +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Autofac.Pooling.Tests.Common; namespace Autofac.Pooling.Test; -public class PoolingTest +public class PoolingTests { [Fact] public void CanRegisterPooledService() @@ -13,9 +15,10 @@ public void CanRegisterPooledService() var activateCounter = 0; // Register a pooled instance. OnActivated only fires when actual instances of the component are created. - builder.RegisterType().As() - .PooledInstancePerLifetimeScope() - .OnActivated(args => activateCounter++); + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope() + .OnActivated(args => activateCounter++); var container = builder.Build(); diff --git a/test/Autofac.Pooling.Test/RegistrationExtensionsNullGuardTests.cs b/test/Autofac.Pooling.Test/RegistrationExtensionsNullGuardTests.cs index 1e46a19..9b40557 100644 --- a/test/Autofac.Pooling.Test/RegistrationExtensionsNullGuardTests.cs +++ b/test/Autofac.Pooling.Test/RegistrationExtensionsNullGuardTests.cs @@ -1,11 +1,9 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System; using Autofac.Builder; using Autofac.Pooling.Tests.Common; using Microsoft.Extensions.ObjectPool; -using Xunit; namespace Autofac.Pooling.Test; diff --git a/test/Autofac.Pooling.Test/RegistrationExtensionsTests.cs b/test/Autofac.Pooling.Test/RegistrationExtensionsTests.cs index 5407d86..fb5895a 100644 --- a/test/Autofac.Pooling.Test/RegistrationExtensionsTests.cs +++ b/test/Autofac.Pooling.Test/RegistrationExtensionsTests.cs @@ -1,7 +1,8 @@ -using System; +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + using Autofac.Builder; using Autofac.Pooling.Tests.Common; -using Xunit; namespace Autofac.Pooling.Test; From 75a16187fafe3d45e4eb64955da8a00b372bcca5 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Wed, 17 Jun 2026 09:11:17 -0700 Subject: [PATCH 5/8] Unify build metadata with core Autofac. --- .gitignore | 4 +- .vscode/extensions.json | 3 +- .vscode/settings.json | 23 +++++++++- .vscode/tasks.json | 72 ++++++++++++++++++++++++++++++-- LICENSE | 2 +- build/CodeAnalysisDictionary.xml | 47 --------------------- codecov.yml | 7 +++- default.proj | 4 +- 8 files changed, 104 insertions(+), 58 deletions(-) delete mode 100644 build/CodeAnalysisDictionary.xml diff --git a/.gitignore b/.gitignore index 8eb87ef..01110cd 100644 --- a/.gitignore +++ b/.gitignore @@ -109,7 +109,6 @@ publish/ *.pubxml # NuGet Packages Directory -## TODO: If you have NuGet Package Restore enabled, uncomment the next line packages/ # Windows Azure Build Output @@ -123,6 +122,8 @@ AppPackages/ sql/ *.Cache ClientBin/ +[Ss]tyle[Cc]op.* +!stylecop.json ~$* *~ *.dbmdl @@ -132,6 +133,7 @@ node_modules/ bower_components/ wwwroot/ project.lock.json +*.Designer.cs # RIA/Silverlight projects Generated_Code/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json index b5177ea..7d7c161 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,6 +2,7 @@ "recommendations": [ "davidanson.vscode-markdownlint", "editorconfig.editorconfig", - "ms-dotnettools.csharp" + "ms-dotnettools.csdevkit", + "travisillig.vscode-json-stable-stringify" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 765e7d3..d63e2f0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,26 @@ "coverage-gutters.coverageFileNames": [ "**/coverage.cobertura.xml" ], - "omnisharp.enableEditorConfigSupport": true + "dotnet.defaultSolution": "Autofac.Pooling.sln", + "dotnet.unitTestDebuggingOptions": { + "enableStepFiltering": false, + "justMyCode": false, + "requireExactSource": false, + "sourceLinkOptions": { + "*": { + "enabled": true + } + }, + "suppressJITOptimizations": true, + "symbolOptions": { + "searchNuGetOrgSymbolServer": true + } + }, + "explorer.fileNesting.enabled": true, + "explorer.fileNesting.patterns": { + "*.resx": "$(capture).*.resx, $(capture).designer.cs, $(capture).designer.vb" + }, + "files.watcherExclude": { + "**/target": true + } } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 70e1c3b..6d0f37a 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,9 +1,37 @@ { + "linux": { + "options": { + "shell": { + "args": [ + "-NoProfile", + "-Command" + ], + "executable": "pwsh" + } + } + }, + "osx": { + "options": { + "shell": { + "args": [ + "-NoProfile", + "-Command" + ], + "executable": "/usr/local/bin/pwsh" + } + } + }, "tasks": [ + { + "command": "If (Test-Path ${workspaceFolder}/artifacts/logs) { Remove-Item ${workspaceFolder}/artifacts/logs -Recurse -Force }; New-Item -Path ${workspaceFolder}/artifacts/logs -ItemType Directory -Force | Out-Null", + "label": "create log directory", + "type": "shell" + }, { "args": [ "build", "Autofac.Pooling.sln", + "--tl:off", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], @@ -13,12 +41,48 @@ "kind": "build" }, "label": "build", - "presentation": { - "reveal": "silent" - }, "problemMatcher": "$msCompile", "type": "shell" + }, + { + "args": [ + "test", + "${workspaceFolder}/Autofac.Pooling.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary", + "--results-directory", + "artifacts/logs", + "--logger:trx", + "--collect:XPlat Code Coverage", + "--settings:build/Coverage.runsettings", + "--filter", + "FullyQualifiedName!~Bench" + ], + "command": "dotnet", + "dependsOn": [ + "create log directory" + ], + "group": { + "isDefault": true, + "kind": "test" + }, + "label": "test", + "problemMatcher": "$msCompile", + "type": "process" } ], - "version": "2.0.0" + "version": "2.0.0", + "windows": { + "options": { + "shell": { + "args": [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command" + ], + "executable": "pwsh.exe" + } + } + } } diff --git a/LICENSE b/LICENSE index e4f076c..5752383 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -MIT License +The MIT License (MIT) Copyright (c) 2020 Autofac Project diff --git a/build/CodeAnalysisDictionary.xml b/build/CodeAnalysisDictionary.xml deleted file mode 100644 index 0f1b469..0000000 --- a/build/CodeAnalysisDictionary.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - Api - Autofac - autowired - autowiring - composable - configurator - Ioc - Mef - Moq - multitenancy - Mvc - Mvx - Mvvm - startable - Owin - - - - - diff --git a/codecov.yml b/codecov.yml index 810cb8a..4bca0d7 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,3 +1,8 @@ codecov: branch: develop - require_ci_to_pass: yes + require_ci_to_pass: true +coverage: + status: + project: + default: + threshold: 1% diff --git a/default.proj b/default.proj index 252cd17..602821f 100644 --- a/default.proj +++ b/default.proj @@ -57,8 +57,8 @@ - - + + From 6a48f72003b1787b23d6351c8fe06dcc97bd05f2 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Wed, 17 Jun 2026 09:33:41 -0700 Subject: [PATCH 6/8] Test cleanup. --- test/Autofac.Pooling.Test/ConcurrencyTests.cs | 10 +- .../CustomProviderTests.cs | 12 +- .../DefaultPooledRegistrationPolicyTests.cs | 22 +-- .../Examples/Caching/ConnectionConsumer.cs | 2 +- .../Examples/Caching/MyCustomConnection.cs | 2 +- .../FactoryOverloadTests.cs | 4 +- test/Autofac.Pooling.Test/PoolServiceTests.cs | 12 +- .../RegistrationExtensionsNullGuardTests.cs | 162 ------------------ .../RegistrationExtensionsTests.cs | 156 ++++++++++++++++- 9 files changed, 182 insertions(+), 200 deletions(-) delete mode 100644 test/Autofac.Pooling.Test/RegistrationExtensionsNullGuardTests.cs diff --git a/test/Autofac.Pooling.Test/ConcurrencyTests.cs b/test/Autofac.Pooling.Test/ConcurrencyTests.cs index 9135101..37ffc2d 100644 --- a/test/Autofac.Pooling.Test/ConcurrencyTests.cs +++ b/test/Autofac.Pooling.Test/ConcurrencyTests.cs @@ -13,8 +13,9 @@ public async Task CanUsePoolConcurrently() { var builder = new ContainerBuilder(); - builder.RegisterType().As() - .PooledInstancePerLifetimeScope(); + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(); var container = builder.Build(); @@ -39,8 +40,9 @@ public async Task CanUsePoolConcurrentlyWithCustomPolicyToBlockOnMaxUsage() using var blockingPolicy = new BlockingPolicy(4); - builder.RegisterType().As() - .PooledInstancePerLifetimeScope(blockingPolicy); + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(blockingPolicy); var container = builder.Build(); diff --git a/test/Autofac.Pooling.Test/CustomProviderTests.cs b/test/Autofac.Pooling.Test/CustomProviderTests.cs index 5d29a95..8c88bfe 100644 --- a/test/Autofac.Pooling.Test/CustomProviderTests.cs +++ b/test/Autofac.Pooling.Test/CustomProviderTests.cs @@ -292,7 +292,7 @@ public void Provider_DisposableCustomPoolDisposedAtShutdown() } [Fact] - public void Provider_NullProviderFactoryThrows() + public void Provider_NullProviderFactory() { var builder = new ContainerBuilder(); @@ -305,7 +305,7 @@ public void Provider_NullProviderFactoryThrows() } [Fact] - public void Provider_NullProviderFactoryWithPolicyThrows() + public void Provider_NullProviderFactoryWithPolicy() { var builder = new ContainerBuilder(); @@ -315,11 +315,11 @@ public void Provider_NullProviderFactoryWithPolicyThrows() Assert.Throws(() => reg.PooledInstancePerLifetimeScope( ctx => new DefaultPooledRegistrationPolicy(), - (Func)null!)); + null!)); } [Fact] - public void Provider_NullPolicyFactoryWithProviderThrows() + public void Provider_NullPolicyFactoryWithProvider() { var builder = new ContainerBuilder(); @@ -328,12 +328,12 @@ public void Provider_NullPolicyFactoryWithProviderThrows() Assert.Throws(() => reg.PooledInstancePerLifetimeScope( - (Func>)null!, + null!, ctx => new TrackingObjectPoolProvider())); } [Fact] - public void Provider_NullProviderFactoryWithMatchingScopeThrows() + public void Provider_NullProviderFactoryWithMatchingScope() { var builder = new ContainerBuilder(); diff --git a/test/Autofac.Pooling.Test/DefaultPooledRegistrationPolicyTests.cs b/test/Autofac.Pooling.Test/DefaultPooledRegistrationPolicyTests.cs index 3d8738c..6716939 100644 --- a/test/Autofac.Pooling.Test/DefaultPooledRegistrationPolicyTests.cs +++ b/test/Autofac.Pooling.Test/DefaultPooledRegistrationPolicyTests.cs @@ -8,23 +8,23 @@ namespace Autofac.Pooling.Test; public class DefaultPooledRegistrationPolicyTests { [Fact] - public void DefaultConstructorUsesTwiceProcessorCount() + public void Ctor_MaximumRetainedUsesSuppliedValue() { - var policy = new DefaultPooledRegistrationPolicy(); + var policy = new DefaultPooledRegistrationPolicy(3); - Assert.Equal(Environment.ProcessorCount * 2, policy.MaximumRetained); + Assert.Equal(3, policy.MaximumRetained); } [Fact] - public void MaximumRetainedConstructorUsesSuppliedValue() + public void Ctor_MaximumRetainedUsesTwiceProcessorCount() { - var policy = new DefaultPooledRegistrationPolicy(3); + var policy = new DefaultPooledRegistrationPolicy(); - Assert.Equal(3, policy.MaximumRetained); + Assert.Equal(Environment.ProcessorCount * 2, policy.MaximumRetained); } [Fact] - public void ZeroMaximumRetainedIsAllowed() + public void Ctor_ZeroMaximumRetainedIsAllowed() { var policy = new DefaultPooledRegistrationPolicy(0); @@ -32,14 +32,14 @@ public void ZeroMaximumRetainedIsAllowed() } [Fact] - public void NegativeMaximumRetainedThrows() + public void Ctor_NegativeMaximumRetainedThrows() { Assert.Throws(() => new DefaultPooledRegistrationPolicy(-1)); } [Fact] - public void GetInvokesTheGetFromPoolCallback() + public void Get_InvokesTheGetFromPoolCallback() { var policy = new DefaultPooledRegistrationPolicy(); using var expected = new PooledComponent(); @@ -50,7 +50,7 @@ public void GetInvokesTheGetFromPoolCallback() } [Fact] - public void GetWithNullGetFromPoolThrows() + public void Get_NullGetFromPool() { var policy = new DefaultPooledRegistrationPolicy(); @@ -59,7 +59,7 @@ public void GetWithNullGetFromPoolThrows() } [Fact] - public void ReturnAlwaysAcceptsTheInstance() + public void Return_AlwaysAcceptsTheInstance() { var policy = new DefaultPooledRegistrationPolicy(); using var instance = new PooledComponent(); diff --git a/test/Autofac.Pooling.Test/Examples/Caching/ConnectionConsumer.cs b/test/Autofac.Pooling.Test/Examples/Caching/ConnectionConsumer.cs index 5a9ccd7..f583393 100644 --- a/test/Autofac.Pooling.Test/Examples/Caching/ConnectionConsumer.cs +++ b/test/Autofac.Pooling.Test/Examples/Caching/ConnectionConsumer.cs @@ -1,4 +1,4 @@ -// Copyright (c) Autofac Project. All rights reserved. +// Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. namespace Autofac.Pooling.Test.Examples.Caching; diff --git a/test/Autofac.Pooling.Test/Examples/Caching/MyCustomConnection.cs b/test/Autofac.Pooling.Test/Examples/Caching/MyCustomConnection.cs index a5b75c5..b902af8 100644 --- a/test/Autofac.Pooling.Test/Examples/Caching/MyCustomConnection.cs +++ b/test/Autofac.Pooling.Test/Examples/Caching/MyCustomConnection.cs @@ -1,4 +1,4 @@ -// Copyright (c) Autofac Project. All rights reserved. +// Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. using Autofac.Core; diff --git a/test/Autofac.Pooling.Test/FactoryOverloadTests.cs b/test/Autofac.Pooling.Test/FactoryOverloadTests.cs index 51f5e3d..641926d 100644 --- a/test/Autofac.Pooling.Test/FactoryOverloadTests.cs +++ b/test/Autofac.Pooling.Test/FactoryOverloadTests.cs @@ -208,7 +208,7 @@ public void PolicyFactory_DisposableRegistrationsDisposedWhenContainerIsDisposed } [Fact] - public void PolicyFactory_NullPolicyFactoryThrows() + public void PolicyFactory_NullPolicyFactory() { var builder = new ContainerBuilder(); @@ -221,7 +221,7 @@ public void PolicyFactory_NullPolicyFactoryThrows() } [Fact] - public void PolicyFactory_NullPolicyFactoryWithMatchingScopeThrows() + public void PolicyFactory_NullPolicyFactoryWithMatchingScope() { var builder = new ContainerBuilder(); diff --git a/test/Autofac.Pooling.Test/PoolServiceTests.cs b/test/Autofac.Pooling.Test/PoolServiceTests.cs index 581ef4a..33c44ea 100644 --- a/test/Autofac.Pooling.Test/PoolServiceTests.cs +++ b/test/Autofac.Pooling.Test/PoolServiceTests.cs @@ -9,7 +9,7 @@ namespace Autofac.Pooling.Test; public class PoolServiceTests { [Fact] - public void DescriptionIncludesTheLimitType() + public void Ctor_DescriptionIncludesTheLimitType() { var registration = RegistrationFor(); @@ -19,7 +19,7 @@ public void DescriptionIncludesTheLimitType() } [Fact] - public void EqualsIsTrueForTheSameUnderlyingRegistration() + public void Equals_SameUnderlyingRegistration() { var registration = RegistrationFor(); @@ -31,7 +31,7 @@ public void EqualsIsTrueForTheSameUnderlyingRegistration() } [Fact] - public void EqualsIsFalseForDifferentRegistrations() + public void Equals_DifferentRegistrations() { var first = new PoolService(RegistrationFor()); var second = new PoolService(RegistrationFor()); @@ -40,7 +40,7 @@ public void EqualsIsFalseForDifferentRegistrations() } [Fact] - public void EqualsObjectMatchesAnotherPoolService() + public void EqualsObject_MatchesAnotherPoolService() { var registration = RegistrationFor(); @@ -51,7 +51,7 @@ public void EqualsObjectMatchesAnotherPoolService() } [Fact] - public void EqualsObjectIsFalseForUnrelatedType() + public void EqualsObject_UnrelatedType() { var service = new PoolService(RegistrationFor()); @@ -60,7 +60,7 @@ public void EqualsObjectIsFalseForUnrelatedType() [Fact] [SuppressMessage("CA1508", "CA1508", Justification = "Deliberately exercising the Equals null branch for coverage.")] - public void EqualsIsFalseForNull() + public void EqualsObject_Null() { var service = new PoolService(RegistrationFor()); diff --git a/test/Autofac.Pooling.Test/RegistrationExtensionsNullGuardTests.cs b/test/Autofac.Pooling.Test/RegistrationExtensionsNullGuardTests.cs deleted file mode 100644 index 9b40557..0000000 --- a/test/Autofac.Pooling.Test/RegistrationExtensionsNullGuardTests.cs +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright (c) Autofac Project. All rights reserved. -// Licensed under the MIT License. See LICENSE in the project root for license information. - -using Autofac.Builder; -using Autofac.Pooling.Tests.Common; -using Microsoft.Extensions.ObjectPool; - -namespace Autofac.Pooling.Test; - -/// -/// Verifies that every public registration overload null-guards its parameters. -/// -public class RegistrationExtensionsNullGuardTests -{ - private static IRegistrationBuilder NullRegistration - => null!; - - [Fact] - public void PerLifetimeScope_NullRegistrationThrows() - { - Assert.Throws(() => - NullRegistration.PooledInstancePerLifetimeScope()); - } - - [Fact] - public void PerLifetimeScope_MaximumRetained_NullRegistrationThrows() - { - Assert.Throws(() => - NullRegistration.PooledInstancePerLifetimeScope(8)); - } - - [Fact] - public void PerLifetimeScope_Policy_NullRegistrationThrows() - { - Assert.Throws(() => - NullRegistration.PooledInstancePerLifetimeScope(new DefaultPooledRegistrationPolicy())); - } - - [Fact] - public void PerLifetimeScope_PolicyFactory_NullRegistrationThrows() - { - Assert.Throws(() => - NullRegistration.PooledInstancePerLifetimeScope( - ctx => new DefaultPooledRegistrationPolicy())); - } - - [Fact] - public void PerLifetimeScope_ProviderFactory_NullRegistrationThrows() - { - Assert.Throws(() => - NullRegistration.PooledInstancePerLifetimeScope( - ctx => new DefaultObjectPoolProvider())); - } - - [Fact] - public void PerLifetimeScope_PolicyAndProviderFactory_NullRegistrationThrows() - { - Assert.Throws(() => - NullRegistration.PooledInstancePerLifetimeScope( - ctx => new DefaultPooledRegistrationPolicy(), - ctx => new DefaultObjectPoolProvider())); - } - - [Fact] - public void PerMatchingLifetimeScope_NullRegistrationThrows() - { - Assert.Throws(() => - NullRegistration.PooledInstancePerMatchingLifetimeScope("tag")); - } - - [Fact] - public void PerMatchingLifetimeScope_MaximumRetained_NullRegistrationThrows() - { - Assert.Throws(() => - NullRegistration.PooledInstancePerMatchingLifetimeScope(8, "tag")); - } - - [Fact] - public void PerMatchingLifetimeScope_Policy_NullRegistrationThrows() - { - Assert.Throws(() => - NullRegistration.PooledInstancePerMatchingLifetimeScope( - new DefaultPooledRegistrationPolicy(), "tag")); - } - - [Fact] - public void PerMatchingLifetimeScope_PolicyFactory_NullRegistrationThrows() - { - Assert.Throws(() => - NullRegistration.PooledInstancePerMatchingLifetimeScope( - ctx => new DefaultPooledRegistrationPolicy(), "tag")); - } - - [Fact] - public void PerMatchingLifetimeScope_ProviderFactory_NullRegistrationThrows() - { - Assert.Throws(() => - NullRegistration.PooledInstancePerMatchingLifetimeScope( - ctx => new DefaultObjectPoolProvider(), "tag")); - } - - [Fact] - public void PerMatchingLifetimeScope_PolicyAndProviderFactory_NullRegistrationThrows() - { - Assert.Throws(() => - NullRegistration.PooledInstancePerMatchingLifetimeScope( - ctx => new DefaultPooledRegistrationPolicy(), - ctx => new DefaultObjectPoolProvider(), - "tag")); - } - - [Fact] - public void PerLifetimeScope_NullPolicyThrows() - { - var reg = new ContainerBuilder() - .RegisterType() - .As(); - - Assert.Throws(() => - reg.PooledInstancePerLifetimeScope((IPooledRegistrationPolicy)null!)); - } - - [Fact] - public void PerMatchingLifetimeScope_NullPolicyThrows() - { - var reg = new ContainerBuilder() - .RegisterType() - .As(); - - Assert.Throws(() => - reg.PooledInstancePerMatchingLifetimeScope( - (IPooledRegistrationPolicy)null!, "tag")); - } - - [Fact] - public void PerMatchingLifetimeScope_PolicyAndProviderFactory_NullPolicyFactoryThrows() - { - var reg = new ContainerBuilder() - .RegisterType() - .As(); - - Assert.Throws(() => - reg.PooledInstancePerMatchingLifetimeScope( - (Func>)null!, - ctx => new DefaultObjectPoolProvider(), - "tag")); - } - - [Fact] - public void PerMatchingLifetimeScope_PolicyAndProviderFactory_NullProviderFactoryThrows() - { - var reg = new ContainerBuilder() - .RegisterType() - .As(); - - Assert.Throws(() => - reg.PooledInstancePerMatchingLifetimeScope( - ctx => new DefaultPooledRegistrationPolicy(), - (Func)null!, - "tag")); - } -} diff --git a/test/Autofac.Pooling.Test/RegistrationExtensionsTests.cs b/test/Autofac.Pooling.Test/RegistrationExtensionsTests.cs index fb5895a..939cf1c 100644 --- a/test/Autofac.Pooling.Test/RegistrationExtensionsTests.cs +++ b/test/Autofac.Pooling.Test/RegistrationExtensionsTests.cs @@ -3,13 +3,17 @@ using Autofac.Builder; using Autofac.Pooling.Tests.Common; +using Microsoft.Extensions.ObjectPool; namespace Autofac.Pooling.Test; public class RegistrationExtensionsTests { + private static IRegistrationBuilder NullRegistration + => null!; + [Fact] - public void RequiresCallbackContainer() + public void PooledInstancePerLifetimeScope_RequiresCallbackContainer() { // Manually create a registration builder, then call AsPooled var regBuilder = RegistrationBuilder.ForType(); @@ -18,7 +22,7 @@ public void RequiresCallbackContainer() } [Fact] - public void MatchingScopeWithMaximumRetainedRegistersAndResolves() + public void PooledInstancePerMatchingLifetimeScope_RegistersAndResolves() { var builder = new ContainerBuilder(); @@ -44,7 +48,7 @@ public void MatchingScopeWithMaximumRetainedRegistersAndResolves() } [Fact] - public void LifetimeOverriddenAfterPoolingSkipsPoolRegistrations() + public void PooledInstancePerLifetimeScope_LifetimeOverriddenAfterPoolingSkipsPoolRegistrations() { // Changing the lifetime after PooledInstancePerLifetimeScope means the // deferred callback should fall back to the original behavior and not @@ -70,7 +74,7 @@ public void LifetimeOverriddenAfterPoolingSkipsPoolRegistrations() } [Fact] - public void DistinctPooledTypesGetDistinctPools() + public void PooledInstancePerLifetimeScope_DistinctPooledTypesGetDistinctPools() { // Exercises PoolService equality/hash-code: two pooled registrations // must resolve to independent pools rather than sharing one. @@ -100,7 +104,7 @@ public void DistinctPooledTypesGetDistinctPools() [Fact] [SuppressMessage("CA2000", "CA2000", Justification = "The container will dispose of the object.")] - public void NoProvidedInstances() + public void PooledInstancePerLifetimeScope_NoProvidedInstances() { var builder = new ContainerBuilder(); @@ -110,7 +114,7 @@ public void NoProvidedInstances() } [Fact] - public void OnReleaseNotCompatible() + public void PooledInstancePerLifetimeScope_OnReleaseNotCompatible() { var builder = new ContainerBuilder(); @@ -122,7 +126,7 @@ public void OnReleaseNotCompatible() } [Fact] - public void OtherLifecycleEventsAreCompatible() + public void PooledInstancePerLifetimeScope_OtherLifecycleEventsAreCompatible() { // A non-OnRelease lifecycle event adds a CoreEventMiddleware of a // different event type; pooling must allow it (only OnRelease is @@ -147,4 +151,142 @@ public void OtherLifecycleEventsAreCompatible() container.Dispose(); } + + [Fact] + public void PooledInstancePerLifetimeScope_NullRegistration() + { + Assert.Throws(() => NullRegistration.PooledInstancePerLifetimeScope()); + } + + [Fact] + public void PooledInstancePerLifetimeScope_NullRegistrationWithMaximumRetained() + { + Assert.Throws(() => NullRegistration.PooledInstancePerLifetimeScope(8)); + } + + [Fact] + public void PooledInstancePerLifetimeScope_NullRegistrationWithPolicy() + { + Assert.Throws(() => NullRegistration.PooledInstancePerLifetimeScope(new DefaultPooledRegistrationPolicy())); + } + + [Fact] + public void PooledInstancePerLifetimeScope_NullRegistrationWithPolicyFactory() + { + Assert.Throws(() => NullRegistration.PooledInstancePerLifetimeScope(ctx => new DefaultPooledRegistrationPolicy())); + } + + [Fact] + public void PooledInstancePerLifetimeScope_NullRegistrationWithProviderFactory() + { + Assert.Throws(() => NullRegistration.PooledInstancePerLifetimeScope(ctx => new DefaultObjectPoolProvider())); + } + + [Fact] + public void PooledInstancePerLifetimeScope_NullRegistrationWithPolicyAndProviderFactory() + { + Assert.Throws(() => + NullRegistration.PooledInstancePerLifetimeScope( + ctx => new DefaultPooledRegistrationPolicy(), + ctx => new DefaultObjectPoolProvider())); + } + + [Fact] + public void PooledInstancePerLifetimeScope_NullPolicy() + { + var reg = new ContainerBuilder() + .RegisterType() + .As(); + + Assert.Throws(() => + reg.PooledInstancePerLifetimeScope((IPooledRegistrationPolicy)null!)); + } + + [Fact] + public void PooledInstancePerMatchingLifetimeScope_NullRegistration() + { + Assert.Throws(() => + NullRegistration.PooledInstancePerMatchingLifetimeScope("tag")); + } + + [Fact] + public void PooledInstancePerMatchingLifetimeScope_NullRegistrationWithMaximumRetained() + { + Assert.Throws(() => + NullRegistration.PooledInstancePerMatchingLifetimeScope(8, "tag")); + } + + [Fact] + public void PooledInstancePerMatchingLifetimeScope_NullRegistrationWithPolicy() + { + Assert.Throws(() => + NullRegistration.PooledInstancePerMatchingLifetimeScope( + new DefaultPooledRegistrationPolicy(), "tag")); + } + + [Fact] + public void PooledInstancePerMatchingLifetimeScope_NullRegistrationWithPolicyFactory() + { + Assert.Throws(() => + NullRegistration.PooledInstancePerMatchingLifetimeScope( + ctx => new DefaultPooledRegistrationPolicy(), "tag")); + } + + [Fact] + public void PooledInstancePerMatchingLifetimeScope_NullRegistrationWithProviderFactory() + { + Assert.Throws(() => + NullRegistration.PooledInstancePerMatchingLifetimeScope( + ctx => new DefaultObjectPoolProvider(), "tag")); + } + + [Fact] + public void PooledInstancePerMatchingLifetimeScope_NullRegistrationWithPolicyAndProviderFactory() + { + Assert.Throws(() => + NullRegistration.PooledInstancePerMatchingLifetimeScope( + ctx => new DefaultPooledRegistrationPolicy(), + ctx => new DefaultObjectPoolProvider(), + "tag")); + } + + [Fact] + public void PooledInstancePerMatchingLifetimeScope_NullPolicy() + { + var reg = new ContainerBuilder() + .RegisterType() + .As(); + + Assert.Throws(() => + reg.PooledInstancePerMatchingLifetimeScope( + (IPooledRegistrationPolicy)null!, "tag")); + } + + [Fact] + public void PooledInstancePerMatchingLifetimeScope_NullPolicyFactory() + { + var reg = new ContainerBuilder() + .RegisterType() + .As(); + + Assert.Throws(() => + reg.PooledInstancePerMatchingLifetimeScope( + null!, + ctx => new DefaultObjectPoolProvider(), + "tag")); + } + + [Fact] + public void PooledInstancePerMatchingLifetimeScope_NullProviderFactory() + { + var reg = new ContainerBuilder() + .RegisterType() + .As(); + + Assert.Throws(() => + reg.PooledInstancePerMatchingLifetimeScope( + ctx => new DefaultPooledRegistrationPolicy(), + null!, + "tag")); + } } From 6af065f27b7d170b6cb4d191b88c88fffa0d13e3 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Wed, 17 Jun 2026 10:45:08 -0700 Subject: [PATCH 7/8] Correct packaging task to unify with other Autofac projects. --- default.proj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/default.proj b/default.proj index 602821f..252cd17 100644 --- a/default.proj +++ b/default.proj @@ -57,8 +57,8 @@ - - + + From bae97d6b5908937420a5805a5f751e6e943331aa Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Wed, 17 Jun 2026 12:28:33 -0700 Subject: [PATCH 8/8] Minor updates to align build with other Autofac packages. --- .vscode/tasks.json | 2 +- README.md | 9 ++++----- src/Autofac.Pooling/Autofac.Pooling.csproj | 4 ++++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 6d0f37a..e7ed437 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -30,7 +30,7 @@ { "args": [ "build", - "Autofac.Pooling.sln", + "${workspaceFolder}/Autofac.Pooling.sln", "--tl:off", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" diff --git a/README.md b/README.md index 8624829..6a6b0b2 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,17 @@ # Autofac.Pooling -[![Build status](https://github.com/autofac/Autofac.Pooling/actions/workflows/main.yml/badge.svg?branch=develop)](https://github.com/autofac/Autofac.Pooling/actions/workflows/main.yml) +Support for pooled instance lifetime scopes in [Autofac](https://autofac.org) dependency injection. Autofac can help you implement a pool of components in your application without you having to write your own pooling implementation, and making these pooled components feel more natural in the world of DI. -Support for pooled instance lifetime scopes in Autofac dependency injection. - -Autofac can help you implement a pool of components in your application without you having to write your own pooling implementation, and making these pooled components feel more natural in the world of DI. +[![Build status](https://github.com/autofac/Autofac.Pooling/actions/workflows/main.yml/badge.svg)](https://github.com/autofac/Autofac.Pooling/actions/workflows/main.yml) [![codecov](https://codecov.io/gh/Autofac/Autofac.Pooling/branch/develop/graph/badge.svg)](https://codecov.io/gh/Autofac/Autofac.Pooling) [![NuGet](https://img.shields.io/nuget/v/Autofac.Pooling.svg)](https://nuget.org/packages/Autofac.Pooling) Please file issues and pull requests for this package in this repository rather than in the Autofac core repo. - [Documentation](https://autofac.readthedocs.io/advanced/pooled-instances.html) - [NuGet](https://www.nuget.org/packages/Autofac.Pooling) - [Contributing](https://autofac.readthedocs.io/en/latest/contributors.html) +- [Open in Visual Studio Code](https://open.vscode.dev/autofac/Autofac.Pooling) -## Getting Started +## Quick Start Once you've added a reference to the `Autofac.Pooling` package, you can start using the new `PooledInstancePerLifetimeScope` and `PooledInstancePerMatchingLifetimeScope` methods: diff --git a/src/Autofac.Pooling/Autofac.Pooling.csproj b/src/Autofac.Pooling/Autofac.Pooling.csproj index eb4f749..df434f4 100644 --- a/src/Autofac.Pooling/Autofac.Pooling.csproj +++ b/src/Autofac.Pooling/Autofac.Pooling.csproj @@ -38,6 +38,10 @@ + + + $(NoWarn);8765;8600;8601;8602;8603;8604 +