Skip to content

About

Ultra-high-performance, modern .NET microkernel framework ported from Cordis

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Cordis.NET

A CLR-native implementation of Cordis semantics for .NET 10 / C# 14.

The project intentionally keeps the kernel small. It does not use Microsoft.Extensions.DependencyInjection, Generic Host, MediatR, Rx, MEF, or an actor framework. Cordis owns its own composition semantics.

Semantic model

Cordis.NET is built around six orthogonal primitives:

  1. Context overlay — hierarchical per-capability realm selection, isolate, and intercept.
  2. Service registry — (service, realm) -> provider; provider visibility is independent from plugin lifetime.
  3. Injection + Fiber — missing dependencies keep a plugin Pending; provider changes unload/reload consumers by dependency epoch.
  4. Effect scope — every activation owns a LIFO reversible effect stack and cancellation lifetime.
  5. Typed events — parallel, serial, bail, and waterfall are distinct typed event keys; listeners are effects.
  6. Config tree + Loader — stable IDs, groups, overlays, typed config codecs, plugin sources, and reload.

The core invariant is:

plugin/fiber lifetime != service visibility realm

A provider plugin can publish a service in a realm and a sibling consumer plugin in the same realm can resolve it. Removing/replacing the provider invalidates the consumer's dependency epoch and causes a clean effect unwind/reload.

Projects

src/
├── Cordis.Core/      # BCL-only composition kernel
└── Cordis.Loader/    # config tree, YAML, plugin sources, collectible assemblies

Cordis.Core is intentionally unaware of YAML, files, assemblies, Microsoft DI, and application frameworks.

Capability seam

A service definition owns the vocabulary and capability key:

public abstract class ShellService(Context context) : Service(context)
{
    public static readonly ServiceKey<ShellService> Key = new("shell");

    public abstract ValueTask<string> RunAsync(
        string command,
        CancellationToken cancellationToken);
}

public static class ShellContext
{
    extension(Context context)
    {
        public ShellService Shell => context.Get(ShellService.Key);
    }
}

A provider implements the service and publishes it during its Fiber activation:

public sealed class LocalShellPlugin : CordisPlugin
{
    public override PluginDescriptor Descriptor { get; } =
        PluginDescriptor.Create("shell-local");

    protected override ValueTask ApplyAsync(
        Context context,
        CancellationToken cancellationToken)
    {
        var shell = new LocalShellService(context);
        context.Own(shell);
        context.Provide(ShellService.Key, shell);
        return ValueTask.CompletedTask;
    }
}

A consumer declares the dependency but imports no provider:

public sealed class BashToolPlugin : CordisPlugin
{
    public override PluginDescriptor Descriptor { get; } =
        PluginDescriptor.Create("tool-bash", ShellService.Key.Id);

    protected override ValueTask ApplyAsync(
        Context context,
        CancellationToken cancellationToken)
    {
        // context.Shell is guaranteed available while this Fiber is Active.
        return ValueTask.CompletedTask;
    }
}

Mount order is not semantically significant. If the consumer is mounted first, its Fiber remains Pending; publishing shell later activates it.

Isolation

Context.Isolate() changes only selected capability realms:

var sandbox = root.Isolate(
    ShellService.Key.Id,
    FileSystemService.Key.Id);

Conceptually:

root:
  shell -> realm A
  fs    -> realm A
  llm   -> realm A

sandbox:
  shell -> realm B
  fs    -> realm C
  llm   -> realm A

The physical providers live in the shared registry; Context stores only overlay decisions.

Fiber lifecycle

                 dependency missing
                       |
                       v
                    Pending
                       |
            dependencies satisfied
                       v
                    Loading
                       |
          +------------+------------+
          |                         |
          v                         v
        Active                    Failed
          |
    epoch changed / dispose
          v
       Unloading
          |
     effects reverse LIFO
          v
   Pending or Disposed

Each activation epoch has its own:

  • CancellationToken
  • EffectScope
  • dependency generation snapshot

Provider removal is itself an effect, so consumers are invalidated before the old provider generation is discarded.

Typed events

public static class Events
{
    public static readonly EventKey<TurnStarted> TurnStarted = new("turn/started");
    public static readonly WaterfallEventKey<Prompt> Prompt = new("prompt");
}

ctx.On(Events.TurnStarted, async (_, evt, ct) => { ... });
await ctx.EmitAsync(Events.TurnStarted, evt, ct);

ctx.On(Events.Prompt, async (_, prompt, ct) => prompt with { ... });
prompt = await ctx.WaterfallAsync(Events.Prompt, prompt, ct);

Listeners are owned by the current Fiber effect scope. Once, Prepend, global listeners, event realms, and context filters are supported.

Loader config

Direct-list YAML:

- id: sandbox-world
  name: cordis:group
  group: true
  isolate:
    - shell
    - fs
  children:
    - id: shell
      name: app:shell-docker
      config:
        image: ubuntu:24.04

    - id: bash-tool
      name: app:tool-bash

The loader also accepts an entries: wrapper.

Stable IDs distinguish update from remove+add. A group is a composition node, not a runtime manager. It derives a Context overlay and mounts children into that environment.

Dynamic assemblies

AssemblyPluginSource uses the BCL mechanisms designed for .NET plugins:

  • collectible AssemblyLoadContext
  • AssemblyDependencyResolver

A dynamic plugin assembly exposes exactly one parameterless IPluginModule. The module returns a PluginRegistration containing the plugin factory and config codec.

Assembly unloading is only generation mechanics. Cordis semantics remain:

Fiber unload
-> effect reversal
-> service withdrawal
-> dependent Fiber transitions
-> plugin generation released

Configuration

Cordis.Loader keeps raw config as JsonNode; the plugin boundary owns typed binding through IPluginConfigCodec / JsonPluginConfigCodec<T> and source-generated JsonTypeInfo<T>.

This keeps Loader independent of plugin-specific configuration types.

Build

Requires .NET 10 SDK.

dotnet build Cordis.NET.sln

No test project is included.

About

Ultra-high-performance, modern .NET microkernel framework ported from Cordis

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages