Esox.SharpAndRusty.ObjectPool
1.0.2
dotnet add package Esox.SharpAndRusty.ObjectPool --version 1.0.2
NuGet\Install-Package Esox.SharpAndRusty.ObjectPool -Version 1.0.2
<PackageReference Include="Esox.SharpAndRusty.ObjectPool" Version="1.0.2" />
<PackageVersion Include="Esox.SharpAndRusty.ObjectPool" Version="1.0.2" />
<PackageReference Include="Esox.SharpAndRusty.ObjectPool" />
paket add Esox.SharpAndRusty.ObjectPool --version 1.0.2
#r "nuget: Esox.SharpAndRusty.ObjectPool, 1.0.2"
#:package Esox.SharpAndRusty.ObjectPool@1.0.2
#addin nuget:?package=Esox.SharpAndRusty.ObjectPool&version=1.0.2
#tool nuget:?package=Esox.SharpAndRusty.ObjectPool&version=1.0.2
Esox.SharpAndRusty.ObjectPool
A thread-safe, production-ready generic object pool for .NET 8, .NET 9, and .NET 10 built on a Result-based API (no unexpected exceptions from pool operations). Supports dependency injection, circuit breaking, eviction, lifecycle hooks, OpenTelemetry metrics, health checks, scoped pools, pooling policies, and automatic warm-up.
⚠️ Disclaimer — see Disclaimer at the bottom of this file.
Table of Contents
- Installation
- Quick Start
- Pool Types
- Dependency Injection
- Configuration
- Circuit Breaker
- Eviction
- Lifecycle Hooks
- Warm-Up
- Health Checks
- OpenTelemetry Metrics
- Scoped Pools
- Pooling Policies
- Async Operations
- Result-Based API
- Disclaimer
- Changelog
Installation
dotnet add package Esox.SharpAndRusty.ObjectPool
Targets: net8.0 · net9.0 · net10.0
Quick Start
Standalone (no DI)
// Fixed-size pool from a pre-created list
var pool = new ObjectPool<MyResource>(new List<MyResource>
{
new MyResource(),
new MyResource(),
new MyResource()
});
var result = pool.GetObject();
result.Match(
poolModel =>
{
using (poolModel) // Dispose() returns the object to the pool
{
poolModel.Unwrap().DoWork();
}
},
error => Console.WriteLine($"Pool unavailable: {error.Message}")
);
Dynamic pool (objects created on demand)
var pool = new DynamicObjectPool<HttpClient>(() => new HttpClient());
var result = pool.GetObject();
if (result.IsSuccess)
{
using var model = result.Unwrap();
await model.Unwrap().GetAsync("https://example.com");
}
Pool Types
| Type | Description |
|---|---|
ObjectPool<T> |
Fixed-size pool. Objects are provided at construction. |
DynamicObjectPool<T> |
Grows on demand using a factory. Supports circuit breaking, eviction, and warm-up. |
QueryableObjectPool<T> |
Extends DynamicObjectPool<T> with predicate-based object selection. |
Dependency Injection
Register pools in your IServiceCollection:
// Standard pool with fluent builder
services.AddObjectPool<DbConnection>(builder => builder
.WithFactory(() => new SqlConnection(connectionString))
.WithMaxSize(50)
.WithMaxActiveObjects(20)
.WithDefaultTimeout(TimeSpan.FromSeconds(5)));
// Dynamic pool — fluent chain (returns ObjectPoolBuilder<T>)
services.AddDynamicObjectPool<DbConnection>(sp => CreateConnection())
.WithMaxSize(100)
.WithMaxActiveObjects(50)
.WithDefaultTimeout(TimeSpan.FromSeconds(5))
.WithCircuitBreaker(failureThreshold: 5, openDuration: TimeSpan.FromSeconds(30))
.WithTimeToLive(TimeSpan.FromMinutes(30))
.WithIdleTimeout(TimeSpan.FromMinutes(5))
.WithAutoWarmupPercentage(targetPercentage: 50)
.WithTelemetry(meterName: "MyApp.Pools");
// Dynamic pool — legacy overload with config action (returns IServiceCollection)
services.AddDynamicObjectPool<IDbConnectionFactory>(
sp => sp.GetRequiredService<IDbConnectionFactory>().Create(),
config => config.MaxPoolSize = 100);
// Queryable pool
services.AddQueryableObjectPool<Car>(builder => builder
.WithInitialObjects(carList)
.AsQueryable());
// Multiple pools at once
services.AddObjectPools(pools =>
{
pools.AddPool<HttpClient>(b => b.WithFactory(() => new HttpClient()));
pools.AddDynamicPool<DbConnection>(sp => new SqlConnection(cs));
});
The fluent AddDynamicObjectPool overload chains all configuration directly on the builder and registers warmup hosted services and telemetry meters automatically. The legacy overload (with an Action<PoolConfiguration<T>> parameter) is still supported for backwards compatibility.
Resolve from DI:
public class MyService(IObjectPool<DbConnection> pool)
{
public async Task DoWorkAsync()
{
var result = pool.GetObject();
result.Match(
model => { using (model) model.Unwrap().Execute(); },
err => logger.LogError(err.Message)
);
}
}
Configuration
services.AddObjectPool<MyResource>(builder => builder
.WithFactory(() => new MyResource())
.WithMaxSize(100) // max objects in pool
.WithMaxActiveObjects(50) // max concurrently checked-out objects
.WithDefaultTimeout(TimeSpan.FromSeconds(10)) // async wait timeout
.WithValidation(obj => obj.IsHealthy()) // validate on return
.WithHealthChecks() // register IPoolHealth
.AsQueryable()); // use QueryableObjectPool<T>
Circuit Breaker
Protect factory calls from cascading failures:
services.AddDynamicObjectPool<HttpClient>(sp => CreateClient())
.WithCircuitBreaker(
failureThreshold: 5,
openDuration: TimeSpan.FromSeconds(30))
.WithCircuitBreakerPercentage(
failurePercentageThreshold: 50.0,
minimumThroughput: 20);
Manual control:
var pool = provider.GetRequiredService<DynamicObjectPool<HttpClient>>();
pool.TripCircuitBreaker(); // force open
pool.ResetCircuitBreaker(); // force closed
var stats = pool.GetCircuitBreakerStatistics();
Console.WriteLine(stats.State); // Closed | Open | HalfOpen
Console.WriteLine(stats.FailurePercentage);
Eviction
Automatically remove stale or idle objects:
// Builder-level (recommended)
services.AddDynamicObjectPool<DbConnection>(sp => CreateConnection())
.WithTimeToLive(TimeSpan.FromMinutes(30))
.WithIdleTimeout(TimeSpan.FromMinutes(5))
.WithEviction(
timeToLive: TimeSpan.FromMinutes(30),
idleTimeout: TimeSpan.FromMinutes(5),
evictionInterval: TimeSpan.FromMinutes(1));
// Custom predicate
services.AddDynamicObjectPool<MyResource>(sp => new MyResource())
.WithCustomEviction((obj, meta) =>
meta.LastAccessedAt < DateTime.UtcNow.AddMinutes(-10));
Lifecycle Hooks
Execute code at each stage of an object's life:
services.AddObjectPool<DbConnection>(builder => builder
.WithFactory(() => new SqlConnection(cs))
.WithLifecycleHooks(hooks =>
{
hooks.OnCreate = conn => conn.Open();
hooks.OnReturn = conn => conn.ClearAllPools();
hooks.OnDispose = conn => conn.Close();
hooks.OnAcquire = conn => logger.LogDebug("Connection acquired");
hooks.OnEvict = (conn, reason) => logger.LogDebug("Evicted: {Reason}", reason);
// Async hooks
hooks.OnCreateAsync = async conn => await conn.OpenAsync();
}));
Warm-Up
Pre-populate the pool before accepting traffic:
// Absolute count
services.AddDynamicObjectPool<HttpClient>(sp => new HttpClient())
.WithAutoWarmup(targetSize: 20);
// Percentage of max capacity
services.AddDynamicObjectPool<HttpClient>(sp => new HttpClient())
.WithAutoWarmupPercentage(targetPercentage: 75);
// Multiple pools
services.ConfigurePoolWarmup(warmup =>
{
warmup.WarmupPool<HttpClient>(targetSize: 20);
warmup.WarmupPool<DbConnection>(percentage: 50);
});
Manual warm-up:
var warmer = provider.GetRequiredService<IObjectPoolWarmer<HttpClient>>();
await warmer.WarmUpAsync(20, cancellationToken);
await warmer.WarmUpToPercentageAsync(75, cancellationToken);
var status = warmer.GetWarmupStatus();
Console.WriteLine($"Created {status.ObjectsCreated} in {status.WarmupDuration.TotalMilliseconds}ms");
Health Checks
// ASP.NET Core health checks
builder.Services
.AddHealthChecks()
.AddObjectPoolHealthCheck<DbConnection>("db-pool")
.AddObjectPoolHealthCheck<HttpClient>("http-pool", tags: ["ready"]);
Direct query:
var pool = provider.GetRequiredService<IObjectPool<DbConnection>>();
var health = ((IPoolHealth)pool).GetHealthStatus();
Console.WriteLine(health.IsHealthy);
Console.WriteLine(health.UtilizationPercentage);
health.Warnings.ForEach(w => Console.WriteLine(w));
OpenTelemetry Metrics
services.AddDynamicObjectPool<HttpClient>(sp => new HttpClient())
.WithTelemetry(meterName: "MyApp.Pools");
// Exposes meters:
// pool.retrieved.total (Counter)
// pool.returned.total (Counter)
// pool.active.current (Gauge)
// pool.available.current (Gauge)
// pool.empty.events (Counter)
// pool.utilization (Gauge)
Prometheus export (no additional dependency):
var pool = provider.GetRequiredService<IPoolMetrics>();
string prometheusText = pool.ExportMetricsPrometheus();
Scoped Pools
Different object sets per logical scope (tenant, user, region):
services.AddScopedObjectPool<DbConnection>(
factory: sp => new SqlConnection(cs),
config: cfg =>
{
cfg.MaxScopes = 50;
cfg.ScopeIdleTimeout = TimeSpan.FromMinutes(15);
cfg.ResolutionStrategy = ScopeResolutionStrategy.PerTenant;
});
// Resolve
var scopedPool = provider.GetRequiredService<ScopedPoolManager<DbConnection>>();
var conn = scopedPool.GetForScope("tenant-42");
Pooling Policies
Control object selection order:
services.AddObjectPool<MyResource>(builder => builder
.WithFactory(() => new MyResource())
.WithPoolingPolicy(PoolingPolicyType.Lifo) // default — Last In First Out (stack)
.WithPoolingPolicy(PoolingPolicyType.Fifo) // First In First Out (queue)
.WithPoolingPolicy(PoolingPolicyType.Priority)); // priority-based selection
Async Operations
// Async get with timeout
var result = await pool.GetObjectAsync(timeout: TimeSpan.FromSeconds(2), cancellationToken);
result.Match(
model => { /* use model */ },
err => logger.LogWarning("Timed out: {Msg}", err.Message));
// Queryable async get
var result = await queryablePool.GetObjectAsync(
car => car.Make == "Ford",
timeout: TimeSpan.FromSeconds(5));
// Async validation on return
services.AddObjectPool<DbConnection>(builder => builder
.WithAsyncValidation(async conn =>
{
return await conn.PingAsync() == PingResult.Ok;
})
.WithAsyncDisposal());
// Async return
await pool.ReturnObjectAsync(model);
Result-Based API
All pool operations return ExtendedResult<T, Error> from Esox.SharpAndRusty — they never throw unexpectedly. Use Match for clean control flow:
pool.GetObject().Match(
model => DoWork(model),
error => HandleError(error.Message));
// Or imperative style
var result = pool.GetObject();
if (result.IsSuccess)
{
using var model = result.Unwrap();
model.Unwrap().DoWork();
}
Cancellation is the only case that throws — OperationCanceledException propagates as per the standard .NET cooperative cancellation protocol.
Disclaimer
This library is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
This project is independently developed and is not affiliated with, endorsed by, or supported by Microsoft or any other organisation. Use in production environments is at your own risk. Always validate the behaviour of pooled resources in your specific use case, particularly when using circuit breakers, eviction policies, and lifecycle hooks, as incorrect configuration can lead to resource exhaustion, data corruption, or connection leaks.
Issues and contributions are welcome via GitHub.
License
MIT — see LICENSE
Changelog
See CHANGELOG.md for the full version history.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Esox.SharpAndRusty (>= 1.6.7)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 8.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.0)
-
net8.0
- Esox.SharpAndRusty (>= 1.6.7)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 8.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.0)
-
net9.0
- Esox.SharpAndRusty (>= 1.6.7)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 8.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.