Remoting
Other processes and devices call your actors through the same IActorSystem. The generator has already written a
serializable contract and a remote proxy for every actor interface, so there’s nothing extra to declare.
Server
Section titled “Server”dotnet add package Shiny.Actors.HttpServerThe server is Shiny.Net.HttpServer, so it runs on a phone too.
builder.Services.AddShinyActors(actors => actors.ServeOverHttp( expose => expose .Expose<ICounter>() // nothing is reachable unless exposed .ExposeStream<OrderPlaced>(), // publish + server-sent events http => { http.AddAuthentication().AddApiKey(o => o.AddKey(key, "client")); http.AddAuthorization(_ => { }); http.Configure((HttpServer s) => { s.UseAuthentication(); s.UseAuthorization(); }); }, authorizationPolicies: [] // every actor route requires authorization));Already building the server yourself? server.MapActors(expose => ...).RequireAuthorization() does the mapping alone.
Client
Section titled “Client”The client is in the core package and uses a plain HttpClient:
IActorSystem actors = new RemoteActorSystem(new HttpClient { BaseAddress = new("http://192.168.1.20:8080/actors/") });
await actors.Get<ICounter>("bob").Increment();await foreach (var order in actors.GetStream<OrderPlaced>("store-1").ReadAllAsync(ct)) { ... }How it works
Section titled “How it works”| Wire | POST {prefix}/call/{actor}/{id}/{method}, with a JSON array of arguments in and a JSON result out. Streams: POST/GET (SSE) {prefix}/streams/{type}/{key}. |
| JSON | Every JsonSerializerContext in a project with actors is registered automatically, and primitives are built in. A type crossing the wire without metadata fails at MapActors, not on the first call. |
| Errors | RemoteActorException.StatusCode: 400 bad arguments, 401/403 auth, 404 not exposed, 409 deadlock, 500 the actor threw. Exception details stay hidden unless IncludeExceptionDetails is on. |
| Ids | Any string. Ids that aren’t plain are sent as ~base64url, because %2F and .. don’t survive every proxy. |
| Remote streams | ReadAllAsync and Subscribe reconnect with backoff (1s, up to 30s). Durable streams resume from the last sequence seen. Others miss what was published meanwhile. |
| Tracing & context | traceparent and x-actor-context headers carry the caller’s trace and ActorRequestContext. |
IActorTransport is the seam for a transport other than HTTP. ActorDispatcher does the server side and works with
bytes only.
Finding devices
Section titled “Finding devices”dotnet add package Shiny.Actors.DiscoveryShiny.Actors.Discovery finds actor servers on the local network over mDNS/Bonjour, so nobody has to type an
address:
builder.Services.AddShinyActors(x => x.UseDiscovery());
// server sideawait using var published = await mdns.PublishActorsAsync(DeviceInfo.Current.Name, port);
// client sideawait foreach (var peer in mdns.BrowseActorPeersAsync(ct)) if (peer.IsAvailable) actors = peer.Connect(http => http.DefaultRequestHeaders.Add("X-Api-Key", code));- The server must listen on the LAN (
Address = IPAddress.Any). Shiny.Net.HttpServer binds to loopback by default. - On Apple platforms, add
_shinyactors._tcptoNSBonjourServices, plus anNSLocalNetworkUsageDescription.


