Native AOT-first, Orleans-compatible distributed actor framework for .NET 10.
Quark follows the Orleans mental model — Grain, Silo, Client, Placement, Persistence — while being built from the ground up for AOT compilation, per-call DI scoping, and lean memory footprints. It achieves Orleans API compatibility at three tiers: drop-in (identical attributes and interfaces), minor-change (same concept, different DI wiring), and Quark-native (new capabilities without direct Orleans equivalents).
Quark is built as an engine, not a collection of actor-shaped conventions. The runtime owns activation identity, mailbox ordering, placement, state lifetime, timers, resource cleanup, persistence boundaries, and diagnostics. User code supplies behavior through explicit APIs that the engine can schedule, observe, persist, and validate.
That leads to a deliberately strong model:
- Behavior is execution logic, not grain state. Grain behaviors are POCOs resolved from a fresh
IServiceScopefor each call, invoked, then discarded. - State must be visible to the engine. Cross-call state belongs in
IActivationMemory<T>,IPersistentActivationMemory<T>,IPersistentState<T>,IManagedActivationMemory<T>, journals, or explicit external services. - AOT is a design constraint, not an optimization. Quark favors source generation, explicit registration, and analyzers over runtime discovery and reflection-heavy magic.
- Compatibility is a bridge. Orleans-style APIs help teams adopt Quark, but the core runtime remains Quark-native: activation-shell owned, generated, trim-safe, and lifecycle-driven.
In short: Orleans-compatible at the edge, Quark-engineered at the core.
Two wiki pages make this case in depth — and honestly:
- Why Quark — positioning vs Orleans and Akka.NET, the AOT trade-offs, per-call construction costs, and when you should not choose Quark.
- Lifecycle and Failure Semantics — the engine contract: what lives how long, and exactly what happens when a behavior throws, an activation fails, a timer faults, or a transaction half-commits.
Project status: pre-1.0. The failure, delivery, and performance contracts are documented and being hardened in the open — see the roadmap epics: Track A — security, Track B — reliability, Track C — benchmarks, Track D — CI/process, and the v1.0 engine epic. If you need years of production burn-in today, Orleans remains the honest recommendation; choose Quark when Native AOT deployment and an explicit, analyzable engine contract are what you're optimizing for.
- Virtual actor model with grain behaviors (POCO, no base class required)
- Per-call DI scoping — fresh
IServiceScopeper grain method call IActivationMemory<T>andIPersistentActivationMemory<T>for in-memory and durable state[PersistentState]attribute injection (Orleans-compatible)JournaledGrain<TState,TEvent>event sourcing- Durable grain reminders (in-memory + Redis backends)
- In-memory grain timers with mailbox-safe callbacks
- In-memory streams (
IAsyncStream<T>) with implicit subscriptions - TCP gateway client with server-pushed stream delivery
- Multi-silo clustering with distributed grain directory
- TLS transport (mutual TLS supported)
- ACID transactions with 2PC coordinator (
ITransactionalState<T>) - Grain observers (
IGrainObserver+CreateObjectReference<T>) - Idle-timeout grain collector
- OpenTelemetry activity propagation
- Native AOT + trim-safe throughout — zero reflection on hot paths
- Roslyn source generators:
GrainProxyGenerator,BehaviorRegistrationGenerator,SerializerGenerator - AOT-safety analyzers (QRK0001–QRK0003)
// 1. Define a grain interface (shared project)
public interface ICounterGrain : IGrainWithStringKey
{
Task IncrementAsync();
Task<int> GetAsync();
}
// 2. Write a grain behavior (server project)
public sealed class CounterState { public int Count { get; set; } }
public sealed class CounterBehavior : IGrainBehavior, ICounterGrain
{
private readonly IActivationMemory<CounterState> _memory;
public CounterBehavior(IActivationMemory<CounterState> memory) => _memory = memory;
public Task IncrementAsync() { _memory.Value.Count++; return Task.CompletedTask; }
public Task<int> GetAsync() => Task.FromResult(_memory.Value.Count);
}
// 3. Host and call
var host = Host.CreateDefaultBuilder(args)
.UseQuark(silo =>
{
silo.Services.AddQuarkRuntime();
silo.Services.AddTcpTransport();
silo.UseLocalhostClustering(gatewayPort: 30001);
silo.Services.AddMyAssemblyBehaviors(); // BehaviorRegistrationGenerator
})
.UseQuarkClient(client =>
{
client.Services.AddLocalClusterClient();
client.Services.AddGrainProxy<ICounterGrain, CounterGrainProxy>(); // GrainProxyGenerator
})
.Build();
var factory = host.Services.GetRequiredService<IGrainFactory>();
var counter = factory.GetGrain<ICounterGrain>("my-counter");
await counter.IncrementAsync();
Console.WriteLine(await counter.GetAsync()); // 1# Build entire solution
dotnet build Quark.slnx
# Run all tests
dotnet test Quark.slnx
# Native AOT smoke build (Linux)
dotnet publish src/Quark.Runtime/Quark.Runtime.csproj -f net10.0 -c Release -r linux-x64 /p:PublishAot=true.NET SDK is pinned to 10.0.201 via global.json. Package versions are centrally managed in Directory.Packages.props — do not add Version= attributes to individual <PackageReference> elements.
| Package | Role |
|---|---|
Quark.Core.Abstractions |
IGrain, key interfaces, IGrainFactory, IClusterClient, lifecycle, placement |
Quark.Core |
Host-builder extensions (UseQuark, UseQuarkClient) |
Quark.Server |
Server meta-package — pulls in Quark.Runtime + Quark.Core for hosting silos |
Quark.Runtime |
Silo runtime: activations, invoker, placement, message pump |
Quark.Client |
LocalClusterClient, proxy factory registries |
Quark.Client.Tcp |
TcpGatewayClusterClient, TCP stream push |
Quark.Serialization |
Binary codec runtime (ZigZag + LEB128) |
Quark.Transport.Tcp |
TCP transport with TLS (System.IO.Pipelines) |
Quark.Persistence.* |
IGrainStorage, IPersistentActivationMemory, JournaledGrain, InMemory + Redis providers |
Quark.Reminders.* |
Durable reminders, InMemory + Redis storage |
Quark.Streaming.* |
IAsyncStream<T>, in-memory stream provider |
Quark.Transactions |
ITransactionalState<T>, 2PC coordinator |
Quark.CodeGenerator |
Roslyn source generators |
Quark.Analyzers |
Roslyn analyzers: AOT safety (QRK000x), data isolation (QRK001x), behavior lifecycle (QRK002x), performance (QRK003x) |
Quark.Diagnostics.* |
IQuarkDiagnosticListener event surface, OpenTelemetry instruments, StuckGrainDetector |
Quark.Testing |
In-process TestCluster test harness |
| Sample | Demonstrates |
|---|---|
samples/Adventure |
Grain-to-grain calls, timers, TCP gateway client, multi-grain state |
samples/ChatRoom |
In-memory streams, TCP client stream push, observers, Spectre.Console UI |
samples/Streaming |
In-memory + TCP stream push, implicit subscriptions |
samples/Persistence |
The "Bank" sample — all five state patterns side by side (IPersistentActivationMemory, [PersistentState], JournaledGrain, eager + managed memory) |
samples/Realm |
MMO spatial backbone — placement, timers, streams at scale |
See the Samples wiki page for full walkthroughs.
Full documentation is in the wiki:
- Why Quark
- Architecture
- Lifecycle and Failure Semantics
- Writing Grains
- Persistence
- Serialization
- Streaming
- Timers and Reminders
- Transactions
- Clustering and Transport
- Source Generators
- Testing
- AOT and Trim
- Orleans Migration Guide
- AOT-first. No reflection on hot paths. Source generators emit all proxy, serializer, and registration code at build time.
- Engine-owned lifecycle. The activation shell owns identity, ordering, state lifetime, timers, resources, placement, and deactivation.
- Per-call DI scoping. Each grain method call gets a fresh
IServiceScope, enabling scoped services (DbContext, etc.) without manual workarounds. - Fail fast.
BehaviorStartupValidatorcatches DI misconfiguration at silo startup, not on the first live call. - Explicit registration. No assembly scanning. Every type is registered via explicit extension methods.
- Mailbox unchanged. The
Channel<Func<Task>>single-reader queue is the actor model's correctness guarantee.
