Skip to content
Shiny.NET

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 JsonTypeInfo from your own JsonSerializerContext.
  • AOT, trim and single-file clean, enforced by the build: the sample console app publishes NativeAOT with zero warnings.
GitHub GitHub stars for shinyorg/actors
Downloads NuGet downloads for Shiny.Actors
Frameworks
.NET
.NET MAUI
Blazor
Operating Systems
Android
iOS
macOS
Windows
Linux
Web
Package Description
NuGet package Shiny.Actors The runtime and the remote client. Includes the source generator
NuGet package Shiny.Actors.DocumentDb State, reminders and event logs in Shiny.DocumentDb: SQLite, PostgreSQL, SQL Server, Cosmos DB, IndexedDB and more
NuGet package Shiny.Actors.HttpServer Serve actors and streams over HTTP with Shiny.Net.HttpServer, from a phone if you like
NuGet package Shiny.Actors.Jobs Fire reminders in the background with a Shiny.Jobs job
NuGet package Shiny.Actors.Notifications Turn reminders into OS-scheduled local notifications, on time even when the app isn’t running
NuGet package Shiny.Actors.Discovery Find actor servers on the LAN over mDNS/Bonjour
NuGet package Shiny.Actors.Testing ActorTestHost: fake time, in-memory storage, recorded calls and streams
Terminal window
dotnet add package Shiny.Actors

The 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 known

Everything 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 AddShinyActors again (from a library, say) adds to the same configuration rather than replacing it.
  • Container-built pieces. AddCallFilter<T>(), UseStateProvider<T>() and AddReminderObserver<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.

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 state
partial 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>, ValueTask or ValueTask<T>. A CancellationToken parameter 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.
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.

The repository has three samples:

  • Sample.Maui is 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.Blazor runs entirely in the browser: event-sourced to-do lists stored in IndexedDB, with a stream driving the UI.
  • Sample.Console publishes as NativeAOT with zero trim warnings: an HTTP actor server and client, SQLite, durable streams and reminders in one binary.
claude plugin marketplace add shinyorg/skills
claude plugin install shiny@shiny

One 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.

copilot plugin marketplace add https://github.com/shinyorg/skills
copilot plugin install shiny@shiny

One 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.

View shiny-actors Skill