Oragon.ElasticPool
0.1.1-beta
Prefix Reserved
dotnet add package Oragon.ElasticPool --version 0.1.1-beta
NuGet\Install-Package Oragon.ElasticPool -Version 0.1.1-beta
<PackageReference Include="Oragon.ElasticPool" Version="0.1.1-beta" />
<PackageVersion Include="Oragon.ElasticPool" Version="0.1.1-beta" />
<PackageReference Include="Oragon.ElasticPool" />
paket add Oragon.ElasticPool --version 0.1.1-beta
#r "nuget: Oragon.ElasticPool, 0.1.1-beta"
#:package Oragon.ElasticPool@0.1.1-beta
#addin nuget:?package=Oragon.ElasticPool&version=0.1.1-beta&prerelease
#tool nuget:?package=Oragon.ElasticPool&version=0.1.1-beta&prerelease
Oragon.ElasticPool
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
- Elasticity — composite-signal grow (waiters + sustained utilization % + p95
acquire-wait), hysteretic shrink to a target size after sustained aggregate low
pressure (
Availablehigh,InUselow,Waitingzero) past a cooldown since the last grow. No thrashing. - 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). PluggableIItemFailurePolicy<T>decides discard vs. quarantine vs. custom. - 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
| 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
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.6)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.6)
- Microsoft.Extensions.Options (>= 10.0.6)
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.6)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.6)
- Microsoft.Extensions.Options (>= 10.0.6)
-
net9.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.6)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.6)
- Microsoft.Extensions.Options (>= 10.0.6)
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 |