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/.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/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 f7754b1..d63e2f0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,30 @@ "autofac", "xunit" ], - "omnisharp.enableEditorConfigSupport": true + "coverage-gutters.coverageBaseDir": "artifacts/logs", + "coverage-gutters.coverageFileNames": [ + "**/coverage.cobertura.xml" + ], + "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..e7ed437 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", + "${workspaceFolder}/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/README.md b/README.md index a364880..6a6b0b2 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,19 @@ # 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) +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: +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 +48,104 @@ 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(); + +// 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() + // 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 cache-backed provider that takes its cache from Autofac looks like this: + +```csharp +public sealed class CacheObjectPoolProvider : ObjectPoolProvider +{ + 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 + 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)) + { + // 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(); + } + } +} +``` + +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. 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:** + - 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/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/src/Autofac.Pooling/Autofac.Pooling.csproj b/src/Autofac.Pooling/Autofac.Pooling.csproj index 7954479..df434f4 100644 --- a/src/Autofac.Pooling/Autofac.Pooling.csproj +++ b/src/Autofac.Pooling/Autofac.Pooling.csproj @@ -1,91 +1,85 @@  - + + 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) + + + $(NoWarn);8765;8600;8601;8602;8603;8604 + + - - + + - 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 addfe08..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; @@ -9,48 +8,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 +65,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..460f14f 100644 --- a/src/Autofac.Pooling/PoolGetActivator.cs +++ b/src/Autofac.Pooling/PoolGetActivator.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 System.Globalization; using Autofac.Core; using Autofac.Core.Resolving.Pipeline; -using Microsoft.Extensions.ObjectPool; namespace Autofac.Pooling; @@ -17,24 +15,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 +38,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/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 b0760f2..501e389 100644 --- a/src/Autofac.Pooling/PooledInstanceContext.cs +++ b/src/Autofac.Pooling/PooledInstanceContext.cs @@ -6,18 +6,28 @@ 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 +43,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/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 1e1c84c..23ebb11 100644 --- a/src/Autofac.Pooling/Properties/AssemblyInfo.cs +++ b/src/Autofac.Pooling/Properties/AssemblyInfo.cs @@ -1,12 +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 13c3301..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; @@ -11,6 +8,7 @@ using Autofac.Core.Registration; using Autofac.Core.Resolving.Middleware; using Autofac.Core.Resolving.Pipeline; +using Microsoft.Extensions.ObjectPool; namespace Autofac.Pooling; @@ -20,26 +18,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 +72,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 +129,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 +187,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 +404,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 +482,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 +552,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 +619,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 +688,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 +958,7 @@ private static void RegisterPooled(pooledInstanceService, policyFactory), + new PoolActivator(pooledInstanceService, policyFactory, providerFactory), RootScopeLifetime.Instance, InstanceSharing.Shared, InstanceOwnership.OwnedByLifetimeScope, @@ -597,4 +991,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/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 83c24e7..5d48e46 100644 --- a/test/Autofac.Pooling.Test/Autofac.Pooling.Test.csproj +++ b/test/Autofac.Pooling.Test/Autofac.Pooling.Test.csproj @@ -1,34 +1,48 @@  - net10.0 + net10.0;net8.0 + $(NoWarn);CS1591 + true + ../../Autofac.snk + true + true ../../build/Test.ruleset AllEnabledByDefault true false + 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 new file mode 100644 index 0000000..ab3e1bd --- /dev/null +++ b/test/Autofac.Pooling.Test/Common/Dependency.cs @@ -0,0 +1,11 @@ +// 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 . +/// +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..af708e1 --- /dev/null +++ b/test/Autofac.Pooling.Test/Common/DependentPooledComponent.cs @@ -0,0 +1,46 @@ +// 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; + +/// +/// 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/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 new file mode 100644 index 0000000..9ddd083 --- /dev/null +++ b/test/Autofac.Pooling.Test/Common/TrackingObjectPoolProvider.cs @@ -0,0 +1,117 @@ +// 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 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/ConcurrencyTests.cs b/test/Autofac.Pooling.Test/ConcurrencyTests.cs index a73c2e7..37ffc2d 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; @@ -17,8 +13,9 @@ public async Task CanUsePoolConcurrently() { var builder = new ContainerBuilder(); - builder.RegisterType().As() - .PooledInstancePerLifetimeScope(); + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(); var container = builder.Build(); @@ -43,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(); @@ -66,7 +64,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 new file mode 100644 index 0000000..8c88bfe --- /dev/null +++ b/test/Autofac.Pooling.Test/CustomProviderTests.cs @@ -0,0 +1,398 @@ +// 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; +using Microsoft.Extensions.ObjectPool; + +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_NullProviderFactory() + { + var builder = new ContainerBuilder(); + + var reg = builder.RegisterType() + .As(); + + Assert.Throws(() => + reg.PooledInstancePerLifetimeScope( + (Func)null!)); + } + + [Fact] + public void Provider_NullProviderFactoryWithPolicy() + { + var builder = new ContainerBuilder(); + + var reg = builder.RegisterType() + .As(); + + Assert.Throws(() => + reg.PooledInstancePerLifetimeScope( + ctx => new DefaultPooledRegistrationPolicy(), + null!)); + } + + [Fact] + public void Provider_NullPolicyFactoryWithProvider() + { + var builder = new ContainerBuilder(); + + var reg = builder.RegisterType() + .As(); + + Assert.Throws(() => + reg.PooledInstancePerLifetimeScope( + null!, + ctx => new TrackingObjectPoolProvider())); + } + + [Fact] + public void Provider_NullProviderFactoryWithMatchingScope() + { + 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..6716939 --- /dev/null +++ b/test/Autofac.Pooling.Test/DefaultPooledRegistrationPolicyTests.cs @@ -0,0 +1,69 @@ +// 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 DefaultPooledRegistrationPolicyTests +{ + [Fact] + public void Ctor_MaximumRetainedUsesSuppliedValue() + { + var policy = new DefaultPooledRegistrationPolicy(3); + + Assert.Equal(3, policy.MaximumRetained); + } + + [Fact] + public void Ctor_MaximumRetainedUsesTwiceProcessorCount() + { + var policy = new DefaultPooledRegistrationPolicy(); + + Assert.Equal(Environment.ProcessorCount * 2, policy.MaximumRetained); + } + + [Fact] + public void Ctor_ZeroMaximumRetainedIsAllowed() + { + var policy = new DefaultPooledRegistrationPolicy(0); + + Assert.Equal(0, policy.MaximumRetained); + } + + [Fact] + public void Ctor_NegativeMaximumRetainedThrows() + { + Assert.Throws(() => + new DefaultPooledRegistrationPolicy(-1)); + } + + [Fact] + public void Get_InvokesTheGetFromPoolCallback() + { + var policy = new DefaultPooledRegistrationPolicy(); + using var expected = new PooledComponent(); + + var result = policy.Get(null!, Enumerable.Empty(), () => expected); + + Assert.Same(expected, result); + } + + [Fact] + public void Get_NullGetFromPool() + { + var policy = new DefaultPooledRegistrationPolicy(); + + Assert.Throws(() => + policy.Get(null!, Enumerable.Empty(), null!)); + } + + [Fact] + public void Return_AlwaysAcceptsTheInstance() + { + var policy = new DefaultPooledRegistrationPolicy(); + using var instance = new PooledComponent(); + + Assert.True(policy.Return(instance)); + } +} 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 new file mode 100644 index 0000000..bbc7600 --- /dev/null +++ b/test/Autofac.Pooling.Test/Examples/Caching/CacheBackedPoolExampleTests.cs @@ -0,0 +1,90 @@ +// 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; + +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..cb21988 --- /dev/null +++ b/test/Autofac.Pooling.Test/Examples/Caching/CacheObjectPool.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. + +#nullable enable + +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/ConnectionConsumer.cs b/test/Autofac.Pooling.Test/Examples/Caching/ConnectionConsumer.cs new file mode 100644 index 0000000..f583393 --- /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/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..b902af8 --- /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 e8a25b7..641926d 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; @@ -183,7 +179,36 @@ public void PolicyFactory_NonSingletonPolicySharesSameInstance() } [Fact] - public void PolicyFactory_NullPolicyFactoryThrows() + 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_NullPolicyFactory() { var builder = new ContainerBuilder(); @@ -196,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/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 new file mode 100644 index 0000000..83f41fc --- /dev/null +++ b/test/Autofac.Pooling.Test/PoolActivatorTests.cs @@ -0,0 +1,51 @@ +// 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 Microsoft.Extensions.ObjectPool; + +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..33c44ea --- /dev/null +++ b/test/Autofac.Pooling.Test/PoolServiceTests.cs @@ -0,0 +1,80 @@ +// 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; + +namespace Autofac.Pooling.Test; + +public class PoolServiceTests +{ + [Fact] + public void Ctor_DescriptionIncludesTheLimitType() + { + var registration = RegistrationFor(); + + var service = new PoolService(registration); + + Assert.Contains(typeof(PooledComponent).FullName!, service.Description, StringComparison.Ordinal); + } + + [Fact] + public void Equals_SameUnderlyingRegistration() + { + 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 Equals_DifferentRegistrations() + { + var first = new PoolService(RegistrationFor()); + var second = new PoolService(RegistrationFor()); + + Assert.False(first.Equals(second)); + } + + [Fact] + public void EqualsObject_MatchesAnotherPoolService() + { + var registration = RegistrationFor(); + + var first = new PoolService(registration); + object second = new PoolService(registration); + + Assert.True(first.Equals(second)); + } + + [Fact] + public void EqualsObject_UnrelatedType() + { + 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 EqualsObject_Null() + { + 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/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/RegistrationExtensionsTests.cs b/test/Autofac.Pooling.Test/RegistrationExtensionsTests.cs index b3b7025..939cf1c 100644 --- a/test/Autofac.Pooling.Test/RegistrationExtensionsTests.cs +++ b/test/Autofac.Pooling.Test/RegistrationExtensionsTests.cs @@ -1,14 +1,19 @@ -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; +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(); @@ -16,9 +21,90 @@ public void RequiresCallbackContainer() Assert.Throws(() => regBuilder.PooledInstancePerLifetimeScope()); } + [Fact] + public void PooledInstancePerMatchingLifetimeScope_RegistersAndResolves() + { + 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 PooledInstancePerLifetimeScope_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 PooledInstancePerLifetimeScope_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() + public void PooledInstancePerLifetimeScope_NoProvidedInstances() { var builder = new ContainerBuilder(); @@ -28,7 +114,7 @@ public void NoProvidedInstances() } [Fact] - public void OnReleaseNotCompatible() + public void PooledInstancePerLifetimeScope_OnReleaseNotCompatible() { var builder = new ContainerBuilder(); @@ -38,4 +124,169 @@ public void OnReleaseNotCompatible() Assert.Throws(() => builder.Build()); } + + [Fact] + public void PooledInstancePerLifetimeScope_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(); + } + + [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")); + } }