Orleans observers
Observers let grains call an object hosted by an Orleans client or another grain. They are useful for live, best-effort notifications while the receiver is connected.
Use observers for a small set of known, live callbacks. They are intentionally transient and are not a general replacement for a durable event bus. A client can disconnect without notice, and a recreated observer has a different identity. Use Orleans streams or another durable messaging mechanism when subscriptions or delivery must survive failures or fan out to many independent consumers.
When to choose observers versus streams
Section titled “When to choose observers versus streams”Use a grain observer when the callback target is a specific connected client object or grain and the application only needs a live notification while that connection remains active. Observers carry very little infrastructure: a callback target is registered, and the grain notifies that target directly.
Use an Orleans stream when the application needs multicast delivery, dynamic subscriptions, playback, provider-defined durability, or recovery after a client or grain restarts. A stream can outlive a single activation or connection, while an observer registration is tied to the callback target and must be re-established after reconnect.
The tradeoff is mostly about lifetime and delivery semantics:
- Observers are low-overhead, direct callbacks. They are best for ephemeral status or UI updates, but they are not durable, replayable, or automatically recovered after disconnects.
- Streams add operational cost through their configured provider and provider-specific delivery semantics. Explicit subscriptions can also require durable subscription records. In return, streams support independent subscribers, retained events, and more flexible failure recovery.
See Choose an Orleans messaging abstraction and Orleans streaming APIs for the broader decision guidance.
Define an observer
Section titled “Define an observer”Observer interfaces derive from IGrainObserver:
public interface IChatObserver : IGrainObserver{ Task ReceiveMessage(string room, string message);}
public sealed class ChatObserver : IChatObserver{ public Task ReceiveMessage(string room, string message) { Console.WriteLine($"[{room}] {message}"); return Task.CompletedTask; }}Use asynchronous return types. Avoid async void. Apply OneWayAttribute only when notifications are deliberately best effort and the publisher doesn’t need exceptions or completion.
Create and remove a client observer reference
Section titled “Create and remove a client observer reference”Convert the local object into an addressable reference:
var observer = new ChatObserver();IChatObserver observerReference = grainFactory.CreateObjectReference<IChatObserver>(observer);
IChatRoomGrain room = grainFactory.GetGrain<IChatRoomGrain>("general");
await room.Subscribe(observerReference);Keep a strong reference to the local observer for as long as it should receive calls. When finished, unsubscribe and delete the object reference:
await room.Unsubscribe(observerReference);grainFactory.DeleteObjectReference<IChatObserver>(observerReference);Deleting the reference releases the client-side registration. Failing to delete long-lived registrations can leak resources.
Manage subscriptions in a grain
Section titled “Manage subscriptions in a grain”ObserverManager<T> tracks observers, expires stale entries, and removes observers whose notifications fail:
public sealed class ChatRoomGrain : Grain, IChatRoomGrain{ private readonly ObserverManager<IChatObserver> _observers;
public ChatRoomGrain(ILogger<ChatRoomGrain> logger) { _observers = new( TimeSpan.FromMinutes(5), logger); }
public Task Subscribe(IChatObserver observer) { _observers.Subscribe(observer, observer); return Task.CompletedTask; }
public Task Unsubscribe(IChatObserver observer) { _observers.Unsubscribe(observer); return Task.CompletedTask; }
public Task Publish(string message) { return _observers.Notify( observer => observer.ReceiveMessage( this.GetPrimaryKeyString(), message)); }}The current API is Notify, including the overload that accepts Func<T, U> returning a Task. There is no NotifyAsync method.
Subscriptions expire lazily after ExpirationDuration. Clients should renew before expiry. A notification exception causes ObserverManager<T> to remove that observer; it doesn’t fail the entire publish operation.
Use ObserverManager<T, U> when the subscription identity should differ from the observer reference.
Grain observers
Section titled “Grain observers”A grain can implement an observer interface and pass a reference to itself:
IChatObserver observer = this.AsReference<IChatObserver>();
await room.Subscribe(observer);Don’t call CreateObjectReference for a grain. Grains are already addressable.
Execution and cancellation
Section titled “Execution and cancellation”Calls to one client observer reference execute sequentially and aren’t reentrant. Different observer references can execute concurrently.
Observer methods can accept a CancellationToken parameter. Cancellation remains cooperative and doesn’t make observer delivery durable. See Cancel Orleans grain calls for cancellation semantics.
