Oragon.ElasticPool 0.1.1-beta

Prefix Reserved
This is a prerelease version of Oragon.ElasticPool.
dotnet add package Oragon.ElasticPool --version 0.1.1-beta
                    
NuGet\Install-Package Oragon.ElasticPool -Version 0.1.1-beta
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Oragon.ElasticPool" Version="0.1.1-beta" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Oragon.ElasticPool" Version="0.1.1-beta" />
                    
Directory.Packages.props
<PackageReference Include="Oragon.ElasticPool" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Oragon.ElasticPool --version 0.1.1-beta
                    
#r "nuget: Oragon.ElasticPool, 0.1.1-beta"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Oragon.ElasticPool@0.1.1-beta
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Oragon.ElasticPool&version=0.1.1-beta&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Oragon.ElasticPool&version=0.1.1-beta&prerelease
                    
Install as a Cake Tool

Oragon.ElasticPool

NuGet License: MIT

Generic, elastic, self-healing object pool for .NET 8 / 9 / 10 — with built-in OpenTelemetry.

Oragon.ElasticPool is a generic in-process object pool for expensive-to-create resources (clients, connections, handlers). It grows under sustained pressure, shrinks when idle, and heals itself by detecting and replacing broken items through pluggable lifecycle hooks. Async-first, DI-first, observable.

For RabbitMQ IConnection + IChannel pooling, install the companion package Oragon.ElasticPool.RabbitMQ.

Install

dotnet add package Oragon.ElasticPool

30-second quickstart

Every lifecycle hook ships in two flavors — a synchronous overload (no ValueTask wrapping) and the original asynchronous overload. Pick whichever fits what each hook actually does; mix sync and async freely in the same pool.

Sync hooks (no I/O)

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Oragon.ElasticPool.Abstractions;
using Oragon.ElasticPool.DependencyInjection;

var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddElasticPool<MyExpensiveClient>("default", pool =>
{
    pool.WithBounds(minSize: 1, maxSize: 16, initialSize: 2);
    pool.IdleTimeout(TimeSpan.FromMinutes(2));
    pool.MaxWaiterCount(256); // optional backpressure cap; default is unbounded

    pool.Factory  ((sp, ct) => new MyExpensiveClient());
    pool.BeforeUse((c, ct) => c.IsHealthy ? PoolState.Healthy : PoolState.Unhealthy);
    pool.Release  ((c, ct) => c.Dispose());
});

using var host = builder.Build();
// AddElasticPool registers a *keyed* singleton — match the name above.
var pool = host.Services.GetRequiredKeyedService<IElasticPool<MyExpensiveClient>>("default");

await using var lease = await pool.AcquireAsync();
lease.Value.DoWork();
// On Dispose, the item returns to the pool — or is discarded if BeforeUse said Unhealthy.
var liveItems = pool.Total;   // idle + in-use + being-created
var waiters = pool.Waiting;   // callers parked in AcquireAsync

public sealed class MyExpensiveClient : IDisposable
{
    public bool IsHealthy => true;
    public void DoWork() { /* ... */ }
    public void Dispose() { /* ... */ }
}

Async hooks (when you need I/O)

builder.Services.AddElasticPool<MyExpensiveClient>("default", pool =>
{
    pool.WithBounds(minSize: 1, maxSize: 16, initialSize: 2);

    pool.Factory  (async (sp, ct) => await MyExpensiveClient.CreateAsync(ct));
    pool.BeforeUse(async (c, ct)  => await c.PingAsync(ct) ? PoolState.Healthy : PoolState.Unhealthy);
    pool.Release  (async (c, ct)  => await c.DisposeAsync());
});

The sync overload wraps the result in a completed ValueTask internally — zero allocation on the fast path. Use it whenever the hook doesn't need await.

Why not Microsoft.Extensions.ObjectPool?

Feature Microsoft.Extensions.ObjectPool Oragon.ElasticPool
Min / Max bounds ❌ (only MaximumRetained) ✅
Elastic grow under pressure ❌ ✅ (composite signal: waiters + utilization + p95 wait)
Auto-shrink when pressure drops ❌ ✅ (hysteretic, aggregate-signal driven)
Lifecycle hooks (5 stages) ❌ ✅ (Factory, BeforeUse, Check, AfterUse, Release)
Health check on borrow ❌ ✅ (BeforeUse)
Background health sweep ❌ ✅ (Check + PeriodicTimer + exponential backoff)
Pluggable failure policy ❌ ✅ (IItemFailurePolicy<T>)
Async-first API ❌ (Get() is sync, blocks) ✅ (AcquireAsync returns ValueTask)
Built-in OpenTelemetry Limited ✅ (Meter + ActivitySource + source-gen ILogger)
Layered pools (e.g., channel→conn) N/A ✅ (see Oragon.ElasticPool.RabbitMQ)

Microsoft.Extensions.ObjectPool is great for cheap, stateless, allocation-only pooling (e.g., StringBuilder). Oragon.ElasticPool is for expensive, stateful, lifecycle-sensitive resources where elasticity and health matter.

Three pillars

  1. Elasticity — composite-signal grow (waiters + sustained utilization % + p95 acquire-wait), hysteretic shrink to a target size after sustained aggregate low pressure (Available high, InUse low, Waiting zero) past a cooldown since the last grow. No thrashing.
  2. Auto-healing — five lifecycle hooks at every stage: Factory (create), BeforeUse (validate on borrow), Check (background sweep), AfterUse (validate on return; opt-in), Release (cleanup/dispose). Pluggable IItemFailurePolicy<T> decides discard vs. quarantine vs. custom.
  3. Fluent DX — async-first, DI-first, builder pattern. services.AddElasticPool<T>(name, configure).

OpenTelemetry in 5 lines

Oragon.ElasticPool exposes a Meter and ActivitySource both named "Oragon.ElasticPool". Wire them to any OTel exporter:

using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;

builder.Services
    .AddOpenTelemetry()
    .WithMetrics(m => m.AddMeter("Oragon.ElasticPool").AddConsoleExporter())
    .WithTracing(t => t.AddSource("Oragon.ElasticPool").AddConsoleExporter());
// In production, swap AddConsoleExporter() for AddOtlpExporter() (Aspire / OTel collector / etc.).

The pool emits these metrics out of the box:

Instrument Type Purpose
pool.size Gauge Total items the pool currently owns
pool.available Gauge Items idle and ready to acquire
pool.in_use Gauge Items currently leased
pool.waiting Gauge Waiters queued on AcquireAsync
pool.acquire.count Counter Total acquires
pool.acquire.duration Histogram Time spent in AcquireAsync
pool.factory.failures Counter Factory exceptions
pool.grow.count Counter Times the pool grew
pool.shrink.count Counter Times the pool shrank
pool.health.failures Counter BeforeUse / Check Unhealthy returns

All instruments carry a single pool.name tag (the name passed to AddElasticPool). Cardinality stays bounded.

ActivitySource spans: Acquire, Release, HealthCheck, Grow, Shrink. Guarded by HasListeners() so you pay nothing if nobody is listening.

Lifecycle hooks

builder.Services.AddElasticPool<MyClient>("default", pool =>
{
    pool.Factory  ((sp, ct) => /* create new T */);                         // required (sync or async)
    pool.BeforeUse((c, ct) => /* return Healthy / Unhealthy */);            // optional (sync or async)
    pool.Check    ((c, ct) => /* background sweep — return Healthy / Unhealthy */); // optional (sync or async)
    pool.AfterUse ((c, ct) => /* validate on return; v1 default no-op */);  // optional (sync or async)
    pool.Release  ((c, ct) => /* dispose / close — runs on eviction */);    // optional (sync or async)
});

Each method has both a synchronous and an asynchronous overload. Returning PoolState.Healthy/PoolState.Unhealthy directly (sync) is equivalent to returning ValueTask.FromResult(...) (async) but reads cleaner when no await is involved.

Returning PoolState.Unhealthy from any hook invokes the configured IItemFailurePolicy<T>. The default DiscardAndReplaceFailurePolicy<T> discards the item and replaces it if the pool is below MinSize.

Bursty workload sample

See samples/Oragon.ElasticPool.RabbitMQ.Sample.BurstyPublisher for an end-to-end demo: a publisher that goes from "few/hour" to "100k simultaneous" and back to idle, watching the pool grow, shrink, and self-heal — with metrics visible in any OTel collector (Aspire Dashboard, Grafana, Console exporter).

Multi-targeting

Targets net10.0, net9.0, net8.0. No exclusive .NET 10 APIs without #if NET10_0_OR_GREATER guards.

Versioning & API stability

SemVer 2.0. Public surface enforced via Microsoft.CodeAnalysis.PublicApiAnalyzers: breaking changes require an explicit PublicAPI.Unshipped.txt update or the build fails.

Contributing

https://github.com/oragon/Oragon.ElasticPool

License

MIT

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Oragon.ElasticPool:

Package Downloads
Oragon.ElasticPool.RabbitMQ

RabbitMQ.Client v7+ adapter for Oragon.ElasticPool — layered IConnection + IChannel pools with IsOpen-based health checks, AutomaticRecoveryEnabled override, and built-in OpenTelemetry. Async-first.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.1-beta 96 5/14/2026