Repository navigation
Architecture
This page describes the internal design of CShells: how shells are hosted, how features are discovered, how DI containers are built per shell, and how HTTP requests are routed.
┌─────────────────────────────────────────────────────────────────┐
│ Application (ASP.NET Core / Generic Host) │
│ │
│ AddCShells() ──► IShellBlueprintProvider │
│ │ │
│ ▼ │
│ IShellRegistry │
│ (lazy activation) │
│ ┌──────────┴──────────┐ │
│ ▼ ▼ │
│ IShell A IShell B │
│ (Shell A: IServiceProvider) (Shell B: IServiceProvider) │
└─────────────────────────────────────────────────────────────────┘
A Shell is an isolated execution context. It owns:
- A
ShellId(unique string identifier) - A generation descriptor (
ShellDescriptor) - A list of enabled feature names
- A
ShellSettingsobject with configuration data - An
IShellcontaining the shell's builtIServiceProvider
Shells do not share service instances. Each shell's container starts from a copy of the root application services and then applies its own feature registrations on top.
A Feature is a class that implements IShellFeature (or a sub-interface). It is the unit of modular functionality. Features:
- Register services via
ConfigureServices(IServiceCollection services) - Are discovered automatically by scanning assemblies
- Can declare dependencies on other features
- Can optionally register HTTP endpoints (
IWebShellFeature), middleware (IMiddlewareShellFeature), or post-configuration hooks (IPostConfigureShellServices)
IShellRegistry manages active shell generations:
- Looks up shell blueprints from the configured
IShellBlueprintProvider. - Composes fresh
ShellSettingswhen a shell is activated or reloaded. - Resolves feature dependencies (topological sort).
- Builds an
IServiceProviderper shell generation. - Tracks active generations and coordinates drain/disposal for replaced generations.
IShellBlueprintProvider is the source of shell blueprints. Code-defined shells use the built-in in-memory provider populated by AddShell(...); external sources register a single provider with AddBlueprintProvider(...) or a provider-specific extension. Mutable sources can attach an IShellBlueprintManager for persisted create/update/delete operations.
public readonly record struct ShellId
{
public string Name { get; }
}Unique identifier for a shell. Equality is case-insensitive.
public class ShellSettings
{
public ShellId Id { get; init; }
public IReadOnlyList<string> EnabledFeatures { get; set; }
public IDictionary<string, object> ConfigurationData { get; set; }
public IDictionary<string, Action<IShellFeature>> FeatureConfigurators { get; }
}ConfigurationData holds configuration key/value pairs for the shell (e.g., "WebRouting:Path" → "") which are used to build the shell's IConfiguration. FeatureConfigurators stores code-first delegates applied to feature instances at build time.
public interface IShell
{
ShellDescriptor Descriptor { get; }
ShellLifecycleState State { get; }
IServiceProvider ServiceProvider { get; }
IShellScope BeginScope();
}The runtime representation of a shell generation. Access active generations through IShellRegistry or inject IShell from a shell-scoped service provider.
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class ShellFeatureAttribute(string? name = null) : Attribute
{
public string? Name { get; }
public string? DisplayName { get; set; }
public string? Description { get; set; }
public object[] DependsOn { get; set; } // string or Type elements
public object[] Metadata { get; set; }
}Optional attribute on feature classes. Without it, the feature name is derived from the class name (e.g., WeatherFeature → "WeatherFeature").
Injectable in feature constructors. Provides the shell settings and all discovered feature descriptors, plus a shared property bag:
public class ShellFeatureContext
{
public ShellSettings Settings { get; }
public IReadOnlyCollection<ShellFeatureDescriptor> AllFeatures { get; }
public IDictionary<object, object> Properties { get; } // shared build-time state
}At activation time, the runtime feature catalog scans the configured feature assemblies for:
- Any non-abstract class implementing
IShellFeatureor a sub-interface
Each discovered type is wrapped in a ShellFeatureDescriptor that records:
- The feature name (from
[ShellFeature]or class name) - The display name, description
- The list of dependencies (strings and types)
- The implementing type
For each shell, CShells:
- Takes the shell's list of
EnabledFeaturesnames. - Looks up the corresponding
ShellFeatureDescriptorfor each. - Resolves transitive dependencies.
- Topologically sorts the full feature set.
- Instantiates features in that order using
ActivatorUtilities.CreateInstance.
Features are instantiated with the root IServiceProvider (not the shell's), so constructors may only inject root-level services plus ShellSettings / ShellFeatureContext.
Each shell's IServiceProvider is built by:
- Copying all registrations from the root
IServiceCollection. - Adding
ShellSettings,ShellId,IShell, and shell feature descriptors as singleton registrations. - Building a shell-specific
IConfigurationfromShellSettings.ConfigurationData. - Calling
ConfigureServices(services)on each feature in topological order. - Calling
PostConfigureServices(services)on features implementingIPostConfigureShellServices. - Building the
IServiceProvider.
Services registered in the root application (e.g., ILogger<T>, IHttpClientFactory) are available in every shell's container. Shell-specific services (e.g., IPaymentProcessor) are only available in the shells that have the corresponding feature enabled.
MapShells() inserts ShellMiddleware into the pipeline. For each request, the middleware:
- Runs the registered
IShellResolverstrategies in priority order. - Gets or activates the matching
IShellfromIShellRegistry. - Creates a tracked scope from the shell with
IShell.BeginScope(). - Sets
HttpContext.RequestServicesto the shell-scoped provider. - Dispatches through the shell's composed feature middleware pipeline (if any), then passes control to the next middleware (endpoint routing).
When a shell activates (at startup, lazily on first request, dynamically at runtime, or on reload), its IMiddlewareShellFeatures are composed — in ascending Order, ties preserving feature-dependency order — into a per-shell pipeline stored in ShellMiddlewarePipelineRegistry, keyed by shell name and generation. ShellMiddleware dispatches matching requests through this pipeline before rejoining the host pipeline, so shell feature middleware runs only for that shell's requests and works for shells activated after the host pipeline was built. Shells that activated before MapShells() ran are registered retroactively when MapShells() is called.
Entries are removed when a generation is disposed. ShellMiddleware resolves the pipeline delegate before taking the request's shell scope, so a concurrent disposal (bounded or forced drain) can never silently skip a shell's middleware: a request either holds the delegate — usable for the rest of that request — or fails loudly when the scope is taken. If a shell's middleware cannot be composed (a feature throws during activation), the shell fails closed: a pipeline that answers 503 is registered in its place until the shell is successfully reloaded.
IShellResolver orchestrates multiple IShellResolverStrategy implementations, running them in ascending order value. The first strategy to return a non-null shell name wins. The built-in strategies:
| Strategy | Order | Matches |
|---|---|---|
WebRoutingShellResolver |
0 | Path prefix, hostname, request header, user claim |
DefaultShellResolverStrategy |
1000 | Always returns "Default" (fallback) |
DynamicShellEndpointDataSource maintains the list of endpoints across all shells. When shells are added, updated, or removed at runtime, the data source is updated and the ASP.NET Core routing table is refreshed without an application restart.
CShells publishes lifecycle transitions to registered IShellLifecycleSubscriber implementations. Use subscribers for cross-cutting runtime reactions such as logging, telemetry, endpoint registration, and cleanup coordination.
| Extension Point | Interface | Purpose |
|---|---|---|
| Shell blueprint source | IShellBlueprintProvider |
Load shell blueprints from any backend |
| Shell resolution | IShellResolverStrategy |
Custom per-request shell matching |
| Service exclusions | IShellServiceExclusionProvider |
Prevent root services from being copied into shells |
| Lifecycle events | IShellLifecycleSubscriber |
React to shell lifecycle transitions |
| Post-build config | IPostConfigureShellServices |
Finalize DI registrations after all features run |
| Dependency inference | IInfersDependenciesFrom<T> |
Automatically inherit another feature's dependencies |