Getting Started
Orleans-style virtual actors without the ceremony. An actor is a small object with an id, its own state, and
one call at a time. You never create or destroy one: Get<ICounter>("bob") always works, the actor is activated
on its first call and deactivated when it has been idle a while. Because each actor handles one call at a time,
there are no locks to write.
It runs in one process, anywhere .NET runs: a .NET MAUI app, Blazor WebAssembly, a console app or a server. There is no cluster, silo or placement. Other processes and devices call your actors over HTTP.
- Only
Microsoft.Extensions.*abstractions as dependencies. - Nothing discovered by reflection. A source generator, shipped inside the package, writes the proxies,
wire contracts and registrations. State is serialized through
JsonTypeInfofrom your ownJsonSerializerContext. - AOT, trim and single-file clean, enforced by the build: the sample console app publishes NativeAOT with zero warnings.
| GitHub | |
| Downloads |
Packages
Section titled “Packages”| Package | Description |
|---|---|
| The runtime and the remote client. Includes the source generator | |
| State, reminders and event logs in Shiny.DocumentDb: SQLite, PostgreSQL, SQL Server, Cosmos DB, IndexedDB and more | |
| Serve actors and streams over HTTP with Shiny.Net.HttpServer, from a phone if you like | |
| Fire reminders in the background with a Shiny.Jobs job | |
| Turn reminders into OS-scheduled local notifications, on time even when the app isn’t running | |
| Find actor servers on the LAN over mDNS/Bonjour | |
ActorTestHost: fake time, in-memory storage, recorded calls and streams |
Install
Section titled “Install”dotnet add package Shiny.ActorsThe source generator ships inside the package under analyzers/, so there is nothing else to reference.
builder.Services.AddShinyActors(); // every actor in the app is already knownEverything else hangs off the same builder: storage, streams, filters, and every add-on package.
builder.Services.AddShinyActors(actors => actors .UseDocumentDb(new SqliteDatabaseProvider($"Data Source={path}")) // Shiny.Actors.DocumentDb .AddDurableStream<ChatMessage>() .UseAutoSave() .UseBackgroundReminders() // Shiny.Actors.Jobs .UseReminderNotifications() // Shiny.Actors.Notifications .UseDiscovery() // Shiny.Actors.Discovery .ServeOverHttp(expose => expose.Expose<IChatRoom>()) // Shiny.Actors.HttpServer .Configure(o => o.IdleTimeout = TimeSpan.FromMinutes(2)));- It accumulates. Calling
AddShinyActorsagain (from a library, say) adds to the same configuration rather than replacing it. - Container-built pieces.
AddCallFilter<T>(),UseStateProvider<T>()andAddReminderObserver<T>()are created by the container, so they can take dependencies. - Late configuration.
Configure((options, services) => ...)covers anything that only exists once the container is built.
No container? await using var actors = new ActorSystem(new ActorSystemOptions { ... });. ActorSystemOptions is
the same raw settings object the builder fills in.
Your first actor
Section titled “Your first actor”An actor is an interface deriving from IActor, and a class deriving from Actor (or Actor<TState>) that
implements it.
public interface ICounter : IActor{ ValueTask<int> Increment(int by = 1); [OneWay] Task Reset(); // queued; the caller doesn't wait}
public class CounterState { public int Count { get; set; } }
public class CounterActor(ILogger<CounterActor> logger) : Actor<CounterState>, ICounter{ public async ValueTask<int> Increment(int by) { State.Count += by; // one call at a time - no locks await WriteStateAsync(); return State.Count; }
public Task Reset() => ClearStateAsync().AsTask();}
[JsonSerializable(typeof(CounterState))] // the generator finds this for the actor's statepartial class AppJson : JsonSerializerContext;Call it from anywhere with IActorSystem:
public class CounterViewModel(IActorSystem actors){ public async Task Tap() => Count = await actors.Get<ICounter>("bob").Increment();}- Methods return
Task,Task<T>,ValueTaskorValueTask<T>. ACancellationTokenparameter flows to the actor, and a caller that cancels stops waiting even while its call is queued. [OneWay]methods are queued and the caller doesn’t wait for them. They can’t return a value.- Constructors take dependencies from a DI scope created for each activation.
- The id is any string, and is available inside the actor as
Id.
What you get
Section titled “What you get”| Virtual actors | Get<T>(id) always works. Activation happens on the first call, and deactivation after IdleTimeout (5 min). See Actors & Lifecycle. |
| Turn-based | One call at a time per actor, and calls to different actors run in parallel. Not reentrant by default. |
| Deadlock detection | A call that would wait on itself (A -> B -> A) throws ActorDeadlockException with the path instead of hanging. |
| State | Actor<TState> plus any number of named IActorState<T>s. ETag-checked writes, optional auto-save. |
| State providers | In-memory, files, or Shiny.DocumentDb (SQLite, PostgreSQL, SQL Server, Cosmos DB, IndexedDB, and more). |
| Timers & reminders | Timers tick as turns. Reminders persist, survive restarts, and activate the actor. |
| Concurrency control | [Reentrant], [AlwaysInterleave], [ReadOnly] and [StatelessWorker]. |
| Event sourcing | JournaledActor<TState, TEvent>: state rebuilt from an event log, with snapshots and full history. |
| Migrations | [StateVersion(n)] + AddStateMigration<T>(...) upgrade stored state and upcast stored events as they’re read. |
| Streams | GetStream<T>(key): typed pub/sub. Make one durable and subscribers can replay what they missed. |
| Call filters | IActorCallFilter runs around every call, local or remote. |
| Request context | ActorRequestContext values flow with a call, onward to other actors, and across remote calls. |
| Telemetry | OpenTelemetry-ready traces and metrics from ActivitySource/Meter named Shiny.Actors. |
| Remoting | Call actors on another process or device through the same IActorSystem, over HTTP. |
| Testing | Fake time, in-memory storage, recorded calls, and state you can seed and inspect. |
| Back pressure | MailboxCapacity bounds each mailbox, and callers wait for room. |
Samples
Section titled “Samples”The repository has three samples:
Sample.Mauiis a chat app where every room is an actor. It uses SQLite storage,[AutoSave], reminder notifications, a background job, and shares rooms with nearby devices over the LAN through mDNS and a pairing code. It runs on iOS, Android and Mac Catalyst.Sample.Blazorruns entirely in the browser: event-sourced to-do lists stored in IndexedDB, with a stream driving the UI.Sample.Consolepublishes as NativeAOT with zero trim warnings: an HTTP actor server and client, SQLite, durable streams and reminders in one binary.
Step 1 — Add the marketplace:
claude plugin marketplace add shinyorg/skillsStep 2 — Install the plugin:
claude plugin install shiny@shinyOne plugin installs all 39 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.
Step 1 — Add the marketplace:
copilot plugin marketplace add https://github.com/shinyorg/skillsStep 2 — Install the plugin:
copilot plugin install shiny@shinyOne plugin installs all 39 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.


