All 190 tests passing (100% success rate)
- 83 original core tests
- 12 dependency injection tests
- 9 health check tests
- 11 OpenTelemetry tests
- 16 warm-up tests
- 11 eviction tests
- 16 circuit breaker tests
- 12 lifecycle hooks tests
- 16 scoped pools tests ?
- 4 warm-up DI integration tests
PoolScopeclass for identifying scopes (tenant, user, context)ScopeResolutionStrategyenum (Ambient, HttpContext, DI, Custom)ScopedPoolConfigurationfor configuring scoped pool behaviorScopedPoolStatisticsfor tracking multi-tenant metricsAmbientPoolScopefor AsyncLocal context-based scoping
- Manages multiple pools per scope/tenant
- Automatic inactive scope cleanup
- Per-scope statistics tracking
- Configurable scope resolution strategies
- Thread-safe concurrent scope management
AddScopedObjectPool<T>()- Register scoped pool managerAddTenantScopedObjectPool<T>()- Tenant-specific poolsAddAmbientScopedObjectPool<T>()- Ambient scope poolsAddCustomScopedObjectPool<T>()- Custom scope resolution
- 16 tests covering all scenarios
- Tenant/user/context scoping tested
- Cleanup and lifecycle verified
- DI integration validated
using EsoxSolutions.ObjectPool.DependencyInjection;
using EsoxSolutions.ObjectPool.Scoping;
var builder = WebApplication.CreateBuilder(args);
// Register tenant-scoped pools
builder.Services.AddTenantScopedObjectPool<DbConnection>(
(sp, tenantId) =>
{
var connString = GetConnectionStringForTenant(tenantId);
return new SqlConnection(connString);
});
var app = builder.Build();
app.Run();public class TenantDataService
{
private readonly ScopedPoolManager<DbConnection> _poolManager;
public TenantDataService(ScopedPoolManager<DbConnection> poolManager)
{
_poolManager = poolManager;
}
public async Task<List<Order>> GetOrdersAsync(string tenantId)
{
var scope = PoolScope.FromTenant(tenantId);
using var connection = _poolManager.GetObjectForScope(scope);
// Connection is from tenant-specific pool
var conn = connection.Unwrap();
// ... execute query ...
}
}// Register with ambient scope resolution
builder.Services.AddAmbientScopedObjectPool<HttpClient>(
sp => new HttpClient());
// In middleware - set scope from HTTP headers
app.Use(async (context, next) =>
{
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();
if (!string.IsNullOrEmpty(tenantId))
{
using (AmbientPoolScope.BeginScope(PoolScope.FromTenant(tenantId)))
{
await next();
}
}
else
{
await next();
}
});
// In your service - automatically uses correct scope
public class ApiService
{
private readonly ScopedPoolManager<HttpClient> _poolManager;
public async Task<string> CallApiAsync()
{
// Automatically resolves to current tenant's pool
using var client = _poolManager.GetObject();
// ...
}
}// Register with custom scope resolver
builder.Services.AddCustomScopedObjectPool<ServiceClient>(
(sp, scope) => new ServiceClient(scope.TenantId),
scopeResolver: () =>
{
// Custom logic to resolve current scope
var httpContext = GetCurrentHttpContext();
var tenantId = httpContext.User.FindFirst("tenant_id")?.Value;
var userId = httpContext.User.FindFirst("sub")?.Value;
return new PoolScope($"user:{userId}", tenantId, userId);
});builder.Services.AddScopedObjectPool<DbConnection>(
(sp, scope) =>
{
var config = sp.GetRequiredService<IConfiguration>();
var connString = config[$"ConnectionStrings:Tenant_{scope.TenantId}"];
var connection = new SqlConnection(connString);
connection.Open();
return connection;
},
configurePool: config =>
{
config.MaxPoolSize = 50;
config.MaxActiveObjects = 25;
config.ValidateOnReturn = true;
config.ValidationFunction = obj =>
((SqlConnection)obj).State == ConnectionState.Open;
},
configureScoping: scopeConfig =>
{
scopeConfig.MaxScopes = 100;
scopeConfig.ScopeIdleTimeout = TimeSpan.FromMinutes(30);
scopeConfig.EnableAutomaticCleanup = true;
scopeConfig.CleanupInterval = TimeSpan.FromMinutes(5);
scopeConfig.OnScopeCreated = scope =>
logger.LogInformation("Created pool for scope: {Scope}", scope);
scopeConfig.OnScopeDisposed = scope =>
logger.LogInformation("Disposed pool for scope: {Scope}", scope);
});- Tenant-based: Separate pools per tenant
- User-based: Separate pools per user
- Context-based: Separate pools per request/session/custom context
- Automatic creation of pools for new scopes
- Automatic cleanup of inactive scopes
- Configurable idle timeout
- Maximum scope limits
- HttpContext: Extract from HTTP headers or claims
- Ambient: Use AsyncLocal context
- DependencyInjection: Integrate with scoped services
- Custom: Implement your own resolution logic
- Per-scope statistics
- Global scoped pool statistics
- Access count tracking
- Idle time tracking
var scopingConfig = new ScopedPoolConfiguration
{
// Resolution strategy
ResolutionStrategy = ScopeResolutionStrategy.HttpContext,
CustomScopeResolver = null,
// Capacity limits
MaxScopes = 100, // Max concurrent scopes
// Cleanup settings
ScopeIdleTimeout = TimeSpan.FromMinutes(30), // Idle before cleanup
EnableAutomaticCleanup = true, // Auto cleanup
CleanupInterval = TimeSpan.FromMinutes(5), // Cleanup frequency
DisposePoolsOnCleanup = true, // Dispose pools
// HTTP/Claims settings
TenantHeaderName = "X-Tenant-Id", // HTTP header name
TenantClaimType = "tenant_id", // JWT claim type
// Lifecycle callbacks
OnScopeCreated = scope => { /* ... */ },
OnScopeDisposed = scope => { /* ... */ }
};var stats = scopedPoolManager.GetStatistics();
Console.WriteLine($"Active Scopes: {stats.ActiveScopes}");
Console.WriteLine($"Total Created: {stats.TotalScopesCreated}");
Console.WriteLine($"Peak Scopes: {stats.PeakScopes}");
Console.WriteLine($"Total Objects: {stats.TotalObjects}");
Console.WriteLine($"Avg Objects/Scope: {stats.AverageObjectsPerScope:F2}");
foreach (var kvp in stats.ScopeAccessCounts)
{
Console.WriteLine($"Scope {kvp.Key}: {kvp.Value} accesses");
}var scope = PoolScope.FromTenant("tenant1");
var scopeStats = scopedPoolManager.GetScopeStatistics(scope);
if (scopeStats != null)
{
Console.WriteLine($"Scope ID: {scopeStats["scope_id"]}");
Console.WriteLine($"Tenant ID: {scopeStats["tenant_id"]}");
Console.WriteLine($"Last Access: {scopeStats["last_access"]}");
Console.WriteLine($"Idle Seconds: {scopeStats["idle_seconds"]}");
}// Manually trigger cleanup
scopedPoolManager.TriggerCleanup();
// Remove specific scope
var removed = scopedPoolManager.RemoveScope(scope);
// Get all active scopes
var activeScopes = scopedPoolManager.GetActiveScopes();
foreach (var s in activeScopes)
{
Console.WriteLine($"Active scope: {s}");
}// Startup
builder.Services.AddTenantScopedObjectPool<DbConnection>(
(sp, tenantId) => CreateTenantConnection(tenantId));
// Middleware to extract tenant
app.Use(async (context, next) =>
{
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault()
?? context.User.FindFirst("tenant_id")?.Value
?? "default";
using (AmbientPoolScope.BeginScope(PoolScope.FromTenant(tenantId)))
{
await next();
}
});
// Service automatically uses correct tenant's pool
public class OrderService
{
private readonly ScopedPoolManager<DbConnection> _poolManager;
public async Task<Order> GetOrderAsync(int orderId)
{
using var connection = _poolManager.GetObject(); // Uses current tenant's pool
// ... query order ...
}
}// Register user-scoped pools
builder.Services.AddScopedObjectPool<UserSession>(
(sp, scope) => new UserSession(scope.UserId));
// In your service
public class UserActivityTracker
{
private readonly ScopedPoolManager<UserSession> _sessionManager;
public async Task TrackActivityAsync(string userId, string activity)
{
var scope = PoolScope.FromUser(userId);
using var session = _sessionManager.GetObjectForScope(scope);
session.Unwrap().RecordActivity(activity);
}
}// Register context-scoped pools
builder.Services.AddScopedObjectPool<RequestContext>(
(sp, scope) => new RequestContext(scope.Id),
configureScoping: config =>
{
config.ResolutionStrategy = ScopeResolutionStrategy.Custom;
config.CustomScopeResolver = () =>
{
var requestId = Activity.Current?.Id ?? Guid.NewGuid().ToString();
return PoolScope.FromContext(requestId);
};
});[Fact]
public void MultiTenant_EachTenantGetsOwnPool()
{
// Arrange
var manager = new ScopedPoolManager<Car>(
scope => new DynamicObjectPool<Car>(
() => new Car(scope.TenantId ?? "unknown", "Model")));
var tenant1 = PoolScope.FromTenant("tenant1");
var tenant2 = PoolScope.FromTenant("tenant2");
// Act
using var car1 = manager.GetObjectForScope(tenant1);
using var car2 = manager.GetObjectForScope(tenant2);
// Assert
Assert.Equal("tenant1", car1.Unwrap().Make);
Assert.Equal("tenant2", car2.Unwrap().Make);
}[Fact]
public async Task AmbientScope_IsolatesPoolsCorrectly()
{
var manager = new ScopedPoolManager<HttpClient>(
scope => new DynamicObjectPool<HttpClient>(() => new HttpClient()),
new ScopedPoolConfiguration
{
ResolutionStrategy = ScopeResolutionStrategy.Ambient
});
using (AmbientPoolScope.BeginScope(PoolScope.FromTenant("tenant1")))
{
using var client1 = manager.GetObject();
Assert.NotNull(client1);
}
using (AmbientPoolScope.BeginScope(PoolScope.FromTenant("tenant2")))
{
using var client2 = manager.GetObject();
Assert.NotNull(client2);
}
}- Scope Limits: Set
MaxScopesbased on expected concurrent tenants/users - Cleanup Interval: Balance between memory usage and cleanup overhead
- Idle Timeout: Set based on tenant activity patterns
- Pool Size Per Scope: Configure appropriate
MaxPoolSizeper tenant needs
builder.Services.AddScopedObjectPool<DbConnection>(
(sp, scope) => CreateConnection(scope.TenantId),
configurePool: config =>
{
// Per-tenant pool configuration
config.MaxPoolSize = 25; // 25 connections per tenant
config.MaxActiveObjects = 15; // Max 15 concurrent per tenant
},
configureScoping: scopeConfig =>
{
// Multi-tenant configuration
scopeConfig.MaxScopes = 1000; // Support up to 1000 tenants
scopeConfig.ScopeIdleTimeout = TimeSpan.FromHours(1); // Cleanup after 1hr idle
scopeConfig.CleanupInterval = TimeSpan.FromMinutes(10); // Check every 10min
});- Use Ambient Scope for request-scoped isolation
- Set Appropriate Timeouts to prevent resource exhaustion
- Monitor Scope Statistics to optimize capacity
- Configure Max Scopes based on actual tenant count
- Enable Automatic Cleanup for long-running applications
- Use Lifecycle Callbacks for audit logging
- Test Scope Isolation thoroughly in integration tests
Solution: Reduce MaxScopes or decrease ScopeIdleTimeout
Solution:
- Verify
EnableAutomaticCleanup = true - Check
ScopeIdleTimeoutisn't too long - Manually call
TriggerCleanup()for testing
Solution:
- Verify scope resolution strategy
- Check custom scope resolver logic
- Ensure ambient scope is set correctly
// From tenant ID
var scope = PoolScope.FromTenant("tenant123");
// From user ID
var scope = PoolScope.FromUser("user456", tenantId: "tenant123");
// From context
var scope = PoolScope.FromContext("request-abc-123", tenantId: "tenant123");
// Custom scope with metadata
var scope = new PoolScope("custom-id", "tenant123", "user456");
scope.Metadata["region"] = "us-east-1";
scope.Metadata["environment"] = "production";// Get pool for specific scope
IObjectPool<T> pool = manager.GetPoolForScope(scope);
// Get object from specific scope
using var obj = manager.GetObjectForScope(scope);
// Get pool for current scope (based on resolution strategy)
IObjectPool<T> pool = manager.GetPool();
// Get object from current scope
using var obj = manager.GetObject();
// Management
manager.RemoveScope(scope);
manager.TriggerCleanup();
var scopes = manager.GetActiveScopes();
var stats = manager.GetStatistics();
var scopeStats = manager.GetScopeStatistics(scope);builder.Services.AddScopedObjectPool<UserContext>(
(sp, scope) => new UserContext(scope.UserId),
configureScoping: config =>
{
config.ResolutionStrategy = ScopeResolutionStrategy.Custom;
config.CustomScopeResolver = () =>
{
var httpContext = GetHttpContext();
var userId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var tenantId = httpContext.User.FindFirst("tenant_id")?.Value;
return PoolScope.FromUser(userId ?? "anonymous", tenantId);
};
});// In gRPC service
public class TenantGrpcService : TenantService.TenantServiceBase
{
private readonly ScopedPoolManager<GrpcChannel> _channelManager;
public override async Task<Response> GetData(Request request, ServerCallContext context)
{
var tenantId = context.RequestHeaders.GetValue("x-tenant-id");
var scope = PoolScope.FromTenant(tenantId);
using var channel = _channelManager.GetObjectForScope(scope);
// Use tenant-specific channel
}
}All 16 scoped pool tests verify:
- ? Scope creation and equality
- ? Pool isolation per scope
- ? Ambient scope context management
- ? Custom scope resolution
- ? Automatic cleanup of inactive scopes
- ? Statistics tracking
- ? DI integration
- ? Lifecycle callbacks
- ? Disposal and resource cleanup
Version 4.0.0 - Production Ready ??