CShells 0.0.30
dotnet add package CShells --version 0.0.30
NuGet\Install-Package CShells -Version 0.0.30
<PackageReference Include="CShells" Version="0.0.30" />
<PackageVersion Include="CShells" Version="0.0.30" />
<PackageReference Include="CShells" />
paket add CShells --version 0.0.30
#r "nuget: CShells, 0.0.30"
#:package CShells@0.0.30
#addin nuget:?package=CShells&version=0.0.30
#tool nuget:?package=CShells&version=0.0.30
CShells
A modular multi-tenancy framework for .NET that enables building feature-based applications with isolated services, configuration, and background workers.
Purpose
CShells is the core runtime package that provides blueprint-driven shell activation, cooperative drain-based reload, feature discovery, per-shell DI containers, and configuration-driven multi-tenancy.
Key Features
- Multi-shell architecture — each shell has its own isolated DI container
- Feature-based modularity — features are discovered automatically via attributes
- Dependency resolution — features can depend on other features with topological ordering
- Configuration-driven — shells and their features are configured via
appsettings.jsonor code - Generation lifecycle —
Initializing → Active → Deactivating → Draining → Drained → Disposed - Cooperative reload —
IShellRegistry.ReloadAsync(name)builds the next generation while draining the previous one; in-flight request scopes finish against the old provider - Observable events —
IShellLifecycleSubscriberreceives every state transition - Configurable drain policies — fixed, extensible, and unbounded timeouts
Activation Request Settlement
GetOrActivateAsync returns only after a generation's activation transaction has settled. GetActive and GetAll may expose a published candidate earlier so routing and lifecycle participants can resolve that exact generation while commit is in progress. A concurrent GetOrActivateAsync call waits for the same-name activation to commit or roll back; cancelling that wait does not cancel the activation. Activation participants must not await activation, reload, or unregister for the same shell name from their callbacks.
Settled Active Observation
The built-in IShellRegistry also implements the optional ISettledShellRegistry capability. Cast the resolved registry rather than registering a second service:
if (registry is ISettledShellRegistry settledRegistry)
{
var settled = settledRegistry.GetSettledActive("orders");
// Null means no current generation has completed activation settlement.
}
The synchronous query does not activate a cold shell or wait for an in-progress activation or reload. It returns the current active generation only after completion callbacks and final eligibility checks. While a replacement is provisional it returns null instead of an older generation; after rollback the restored current generation can be observed again. Complete callback errors remain diagnostic-only. The returned shell is a point-in-time observation, not a use lease, and may start draining immediately afterward. GetActive and routing visibility are unchanged. Third-party registries may omit this capability; distinguish that unsupported case from a supported query that returns null.
Installation
dotnet add package CShells
Quick Start
1. Create a Feature
using CShells.Features;
using Microsoft.Extensions.DependencyInjection;
[ShellFeature("Core", DisplayName = "Core Services")]
public class CoreFeature : IShellFeature
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ITimeService, TimeService>();
}
}
2. Configure Shells
{
"CShells": {
"Shells": {
"Default": {
"Features": { "Core": {} }
}
}
}
}
3. Register CShells
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddCShells(cshells =>
cshells.WithConfigurationProvider(builder.Configuration));
var app = builder.Build();
app.Run();
Shell Scopes & Background Work
IShell.BeginScope() returns a tracked IShellScope that (a) exposes a scoped IServiceProvider built from the shell's container and (b) delays drain-handler invocation while the scope is outstanding:
public class ShellBackgroundWorker(IShellRegistry registry) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
foreach (var name in registry.GetBlueprintNames())
{
var shell = registry.GetActive(name);
if (shell is null) continue;
await using var scope = shell.BeginScope();
var service = scope.ServiceProvider.GetService<IMyService>();
service?.Execute();
}
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
}
Code-First Shell Registration
builder.Services.AddCShells(cshells =>
{
cshells.AddShell("Default", shell => shell
.WithFeatures("Core", "Weather")
.WithConfiguration("Theme", "Dark")
.WithConfiguration("MaxItems", "100"));
});
Share Host-Owned Singleton Services
By default, CShells copies root service descriptors into each shell so singleton registrations have independent instances per shell. Opt in when every shell should use the host's singleton object and the host should retain its lifetime ownership:
builder.Services.AddSingleton<IClock, SystemClock>();
builder.Services.AddCShells(cshells => cshells
.ShareSingletonWithShells<IClock>()
.AddShell("Default", shell => shell.WithFeatures("Core")));
The selection includes every unkeyed singleton registration for that service type, in registration order. The root provider resolves and owns those instances; shell providers borrow them without disposing them. The host disposes root-created disposable singletons with its normal provider lifecycle. Instances registered directly by the caller retain the usual caller-owned disposal behavior. Select aliases separately, and use the Type overload when the service type is chosen at runtime.
Keyed registrations are unchanged. A selection must have at least one unkeyed registration and every matching unkeyed registration must be singleton. Open generic selections, matching open-generic registrations, null factory results, and root-only exclusions fail when CShells builds a shell provider. Later shell core and feature registrations keep normal precedence, so this API does not force the host instance to win every single-service resolution.
An explicit unkeyed IEnumerable<IClock> or open-generic IEnumerable<> registration overrides the container-generated enumerable and is rejected when IClock is selected. Keyed enumerable registrations are independent. Repeated AddCShells calls before building the root service provider configure the same builder; finish all configuration before building that provider.
Opt-In Shell Activation Runner
Hosts that own their startup or warmup policy can explicitly register the generic runner:
builder.Services.AddShellActivationRunner();
var run = app.Services.GetRequiredService<IShellActivationRunner>().Start(
["system", "tenant-a"],
retryPolicy: attempt => attempt.AttemptNumber < 4
? ShellActivationRetryDecision.RetryAfter(TimeSpan.FromSeconds(2))
: ShellActivationRetryDecision.Stop);
await run.InitialPass;
foreach (var target in run.Snapshot)
logger.LogInformation("Shell {ShellName}: {Status}", target.ShellName, target.Status);
await run.StopAsync(hostStoppingToken);
Registration is opt-in and TryAdd-style. AddCShells does not register or start this runner, and it adds no hosted service or startup ordering. The runner resolves the registry when the root runner service is first resolved, so registration may appear before or after AddCShells. The runner itself is excluded from shell service copies.
Each run validates and copies its target names, deduplicates them case-insensitively, and performs one serial initial pass in first-occurrence order. InitialPass completes only after every target gets its first attempt; retries and their deadlines start afterward. Without a retry policy, unsuccessful targets are one-shot. A policy chooses Stop or a positive RetryAfter delay. Snapshots contain attempt counts, outcome and generic error codes, timing, failure history, retry state, and a verified generation where available; they do not expose exceptions or their messages. Retry policy and observer inputs may inspect the transient activation exception.
Attempt callbacks and target snapshots also expose ReturnedGeneration, the descriptor generation returned by an owned activation call that succeeds and remains current at verification. This can be populated for custom registry results even when VerifiedGeneration remains null. External satisfaction alone does not set it; an already in-flight owned call can still report its own generation if it later succeeds, without changing the external status or verified generation. Both fields are scalar diagnostics and do not retain a shell or provider.
Only a current, committed concrete CShells Shell can satisfy a target through external snapshot reconciliation; an unknown custom shell implementation cannot. A successful custom registry return is accepted only when it remains current at verification. Once a target is satisfied, it remains terminal even if that shell later drains. StopAsync cancels scheduling and joins work, bounded by its caller token, but it never drains shells. Callbacks must not synchronously wait for StopAsync on their own run.
Per-Shell Initialization & Drain
Register IShellInitializer services for per-shell startup work and IDrainHandler services for cooperative shutdown:
public class PaymentsFeature : IShellFeature
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IPaymentProcessor, StripePaymentProcessor>();
services.AddShellInitializer<RunPaymentMigrations>(
LifecyclePhase.Prepare,
order: 100);
services.AddShellInitializer<StartPaymentProcessor>(
LifecyclePhase.Start,
order: 100);
services.AddTransient<IDrainHandler, PaymentsDrainHandler>();
}
}
Initializers run sequentially during Initializing -> Active. Existing direct IShellInitializer registrations still run in DI-registration order in LifecyclePhase.Default; AddShellInitializer<T>() adds explicit phase/order metadata and registers the initializer as transient unless you have already registered it yourself, in which case your lifetime is preserved. Drain handlers run in parallel during Draining, after all outstanding IShellScope handles have been released or the drain deadline elapses.
Hosts that need to protect resources for a build or live generation can register root IShellGenerationBuildParticipant services. CShells assigns the immutable shell descriptor before blueprint composition, invokes participants after composition and name validation but before catalog initialization, then calls each acquired lease with the exact detailed snapshot used for feature selection before feature construction. Successful snapshot callbacks run in registration order. If one fails, callbacks after it are skipped while every acquired lease is unwound. Participants that fail before returning a lease clean up their own partial acquisition.
For a published shell, leases release in reverse order after the Disposed lifecycle notification and full provider teardown succeed. Failed builds and unpublished initializer candidates release after partial provider cleanup. A lifecycle/provider teardown failure retains unresolved leases in the root registry without retaining the shell or provider; individual release failures retain only the leases that failed. Distinct shell names may run callbacks concurrently. Callbacks must not start activation, reload, or unregister for the same name. This lifecycle hook manages protection ownership but does not perform package deletion or guarantee assembly unloading.
Reload
var result = await registry.ReloadAsync("payments");
// result.NewShell.Descriptor.Generation == previous + 1
// result.Drain (when non-null) is the cooperative drain on the previous generation.
if (result.Drain is not null)
await result.Drain.WaitAsync();
Runtime Feature Catalog Notifications
The public IRuntimeFeatureCatalog remains compatible with custom host implementations. When observing commits, capability-test the resolved catalog instance instead of resolving a separate event service:
if (catalog is IRuntimeFeatureCatalogCommitSource commits)
commits.SnapshotCommitted += snapshot => queueReconciliation(snapshot);
Subscribe before the first catalog initialization to receive its initial commit. Events are not replayed. If you subscribe later, subscribe first and read CurrentSnapshot to reconcile; a refresh may commit during that read, so compare generations to avoid missing or processing a generation twice. The property may also be newer than the exact snapshot in an event. Notifications are delivered in commit order outside the refresh lock, and subscriber exceptions are isolated. Handlers run synchronously, so enqueue lengthy work for later processing.
Learn More
| 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
- CShells.Abstractions (>= 0.0.30)
- JetBrains.Annotations (>= 2025.2.4)
- Microsoft.Extensions.Configuration (>= 10.0.0)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.0)
- Microsoft.Extensions.DependencyInjection (>= 10.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Microsoft.Extensions.DependencyModel (>= 10.0.1)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Logging (>= 10.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.0)
-
net8.0
- CShells.Abstractions (>= 0.0.30)
- JetBrains.Annotations (>= 2025.2.4)
- Microsoft.Extensions.Configuration (>= 9.0.11)
- Microsoft.Extensions.Configuration.Abstractions (>= 9.0.11)
- Microsoft.Extensions.Configuration.Binder (>= 9.0.11)
- Microsoft.Extensions.DependencyInjection (>= 9.0.11)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.11)
- Microsoft.Extensions.DependencyModel (>= 10.0.1)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.11)
- Microsoft.Extensions.Logging (>= 9.0.11)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.11)
-
net9.0
- CShells.Abstractions (>= 0.0.30)
- JetBrains.Annotations (>= 2025.2.4)
- Microsoft.Extensions.Configuration (>= 9.0.11)
- Microsoft.Extensions.Configuration.Abstractions (>= 9.0.11)
- Microsoft.Extensions.Configuration.Binder (>= 9.0.11)
- Microsoft.Extensions.DependencyInjection (>= 9.0.11)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.11)
- Microsoft.Extensions.DependencyModel (>= 10.0.1)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.11)
- Microsoft.Extensions.Logging (>= 9.0.11)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.11)
NuGet packages (9)
Showing the top 5 NuGet packages that depend on CShells:
| Package | Downloads |
|---|---|
|
CShells.AspNetCore
ASP.NET Core integration for CShells. Provides middleware and extensions for shell/tenant resolution based on HTTP context, including host-based and route-based strategies for modular multi-tenant applications. |
|
|
Elsa.Dashboard.Api
Provides operational dashboard API endpoints for Elsa hosts. |
|
|
Elsa.Shells.Api
Provides API endpoints for shell management. |
|
|
CShells.Providers.FluentStorage
FluentStorage integration provider for CShells. Enables loading and persisting shell configurations from various storage backends (Azure Blob, AWS S3, disk, memory, etc.) supported by FluentStorage. |
|
|
Elsa.AI.Persistence.EFCore
Provides EF Core persistence for Elsa AI conversations, proposals and audit records. |
GitHub repositories (1)
Showing the top 1 popular GitHub repositories that depend on CShells:
| Repository | Stars |
|---|---|
|
elsa-workflows/elsa-core
The Workflow Engine for .NET
|
| Version | Downloads | Last Updated |
|---|---|---|
| 0.0.30 | 3 | 10/10/2026 |
| 0.0.29 | 230 | 9/19/2026 |
| 0.0.28 | 57,395 | 6/12/2026 |
| 0.0.27 | 238 | 6/12/2026 |
| 0.0.26 | 223 | 6/12/2026 |
| 0.0.25 | 235 | 6/11/2026 |
| 0.0.24 | 1,332 | 5/15/2026 |
| 0.0.23 | 209 | 5/15/2026 |
| 0.0.22 | 222 | 5/14/2026 |
| 0.0.21 | 246 | 5/12/2026 |
| 0.0.20 | 268 | 5/8/2026 |
| 0.0.19 | 211 | 5/6/2026 |
| 0.0.18 | 219 | 5/2/2026 |
| 0.0.17 | 218 | 4/29/2026 |
| 0.0.16 | 220 | 4/27/2026 |
| 0.0.15 | 204 | 4/27/2026 |
| 0.0.14 | 1,496 | 4/20/2026 |
| 0.0.13 | 205 | 4/17/2026 |
| 0.0.12 | 343 | 3/16/2026 |
| 0.0.11 | 406 | 2/28/2026 |