diff --git a/docs/site/src/content/docs/grains/grain-persistence/index.md b/docs/site/src/content/docs/grains/grain-persistence/index.md index b788694e33a..b3f6e019d97 100644 --- a/docs/site/src/content/docs/grains/grain-persistence/index.md +++ b/docs/site/src/content/docs/grains/grain-persistence/index.md @@ -1,7 +1,7 @@ --- title: Grain persistence description: Persist Orleans grain state using IPersistentState and storage providers. -ms.date: 08/17/2026 +ms.date: 08/21/2026 ms.topic: overview --- @@ -9,7 +9,7 @@ ms.topic: overview Orleans grain persistence stores application state independently of a grain activation. When an activation starts, Orleans reads its configured state records before calling . The grain explicitly writes changes when the operation's durability point is reached. -Persistence is intentionally a record-oriented abstraction, not an object-relational mapper. A grain can use multiple named state records, use different providers for different records, or access a database directly when it needs queries or data models that don't fit grain storage. +Persistence is intentionally a record-oriented abstraction, not an object-relational mapper. A grain can use multiple named state records, use different providers for different records, or access a database directly when it needs queries or data models that don't fit grain storage. See [Model collections of grains](../../how-to/model-grain-collections.md) for registry, partitioned-index, and external query-store patterns. ## Choose a provider diff --git a/docs/site/src/content/docs/grains/index.md b/docs/site/src/content/docs/grains/index.md index 2ec4d2224f9..cd01a06a682 100644 --- a/docs/site/src/content/docs/grains/index.md +++ b/docs/site/src/content/docs/grains/index.md @@ -87,6 +87,7 @@ Use for most grain tests so acti Most grains only need a contract, an implementation, a stable key, and regular request-response calls. Add specialized behavior only when the workload requires it: - [Request scheduling and reentrancy](request-scheduling.md) +- [Model collections of grains](../how-to/model-grain-collections.md) - [Response streaming with IAsyncEnumerable](response-streaming.md) - [Grain timers](timers.md) - [Reminders](reminders.md) diff --git a/docs/site/src/content/docs/how-to/index.md b/docs/site/src/content/docs/how-to/index.md index 94d903e6ba8..5e3aa6fc943 100644 --- a/docs/site/src/content/docs/how-to/index.md +++ b/docs/site/src/content/docs/how-to/index.md @@ -35,6 +35,7 @@ If you are learning Orleans from an empty directory, start with the [tutorials a ## Use Orleans features +- [Model collections of grains](model-grain-collections.md) - [Persist grain state](../grains/grain-persistence/index.md) - [Configure experimental Journaling](../grains/journaling/configuration.md) - [Schedule activation-scoped work with grain timers](../grains/timers.md) diff --git a/docs/site/src/content/docs/how-to/model-grain-collections.md b/docs/site/src/content/docs/how-to/model-grain-collections.md new file mode 100644 index 00000000000..3a4cda7495c --- /dev/null +++ b/docs/site/src/content/docs/how-to/model-grain-collections.md @@ -0,0 +1,89 @@ +--- +title: Model collections of grains +description: Model bounded catalogs, partitioned indexes, query stores, paging, and bulk operations over Orleans grains. +ms.date: 08/25/2026 +ms.topic: how-to +--- + +# Model collections of grains + +Orleans virtualizes individually addressed grains. Applications define collection membership explicitly and choose the storage and consistency model which matches each collection's access patterns. + +A grain key identifies a logical grain, and can create a reference for any valid key. Therefore, a domain catalog or query store determines which keys currently represent application members. Callers select a logical member or shard key; Orleans places activations and routes calls to their current locations. Persisted application catalogs retain membership across deactivation and cluster restarts. + +## Choose a collection shape + +| Requirement | Model | +|---|---| +| A bounded aggregate owned by one domain entity | Store member keys in the owning grain's state | +| Lookup by a known partition key, such as tenant or category | Use one registry or index grain per partition | +| A large hash-partitioned map or set | Route each member to a shard using an application-defined stable hash | +| Ordered paging or range scans | Use range-partitioned index grains or an external query store | +| Ad hoc filtering, sorting, reporting, or full-text search | Query an external read model and resolve the returned grain keys | +| Incremental delivery of one query result | Return from the registry, index, or query grain | + +Collection state usually stores grain keys and the minimum fields needed for routing, filtering, or ordering. Resolve typed [grain references](../grains/grain-references.md) from those keys when invoking members. Persist a grain reference when retaining its interface relationship is useful. + +## Choose grains or stored values + +Model each member as a grain when it has an independent identity, behavior, concurrency boundary, or lifecycle. The collection retains member keys and routes operations to those grains. + +Store ordinary serializable objects in an owning grain's state when they form a bounded aggregate whose operations run through that owner. This provides one activation and persistence boundary for the collection and its values. For large collections of data-oriented records, an external database provides scalable storage and query execution; grains can coordinate domain operations using the returned record identifiers. + +## Keep bounded catalogs in one grain + +An owning grain or registry grain is a direct model for a collection with a known operational bound. A non-reentrant owner processes one request at a time. An awaited state write persists the updated catalog as one record, and the configured provider applies its record-level concurrency checks. + +Return pages or streamed results instead of returning the complete collection. Size the bound using: + +- The storage provider's record-size and request limits. +- The activation memory required to deserialize and index the collection. +- The time required to read, write, and serialize the full state record. +- The response size and timeout budget for callers. + +When membership can grow continuously, partition the catalog before those limits become an operational constraint. + +## Partition membership and indexes + +Partitioned registry grains distribute collection state and request load. Select a routing rule which every caller can reproduce: + +- **Domain partitioning** uses a stable value such as tenant, region, or product category. +- **Hash partitioning** spreads point lookups and unordered membership evenly. +- **Range partitioning** supports ordered traversal and range queries. + +Each membership entry has one owning shard. Keep the shard count and hash algorithm stable, or include a routing version in the shard identity. A resharding workflow can write new-version entries, switch readers, and then retire old-version entries after reconciliation. + +For a query spanning known shards, use a coordinator service or grain to issue calls with bounded concurrency and merge the results. Apply limits for shard fan-out, returned items, elapsed time, and per-call concurrency. This keeps one query from producing an unbounded number of grain calls. + +Treat a bulk operation as a set of shard operations. Include an operation identifier, collect per-shard outcomes, and retry incomplete shards. Use Orleans transactions when the participant count and contention fit the transaction limits and the operation requires one atomic outcome. + +## Define the consistency boundary + +Collection membership and member state often occupy different records. Choose the authority and completion contract before implementing updates: + +| Update model | Completion contract | +|---|---| +| Member data and membership share one grain state record | The completed state write stores both changes in one record | +| Member and index use Orleans transactional state | An [Orleans transaction](../grains/transactions.md) commits both changes atomically | +| One external database owns member data and indexes | A database transaction commits the data and queryable index together | +| Grain state is authoritative and a separate read model is updated asynchronously | A durable event or outbox drives an idempotent projector; queries expose the documented projection lag | + +For uniqueness, route every candidate value to one deterministic index shard. That shard durably reserves the value for one member key before the operation reports success. Include an operation identifier so retries reach the same outcome. + +For asynchronous indexes, retain enough information to replay updates and reconcile the index against its authority. Metrics should report projection lag, failed updates, and reconciliation differences. + +## Query and page results + +Use a database or search service as the query authority when the workload needs ad hoc predicates, sorting, aggregation, or reporting. Query for grain keys and the fields needed to order the result, then resolve grain references for commands or current domain behavior. The [grain persistence](../grains/grain-persistence/index.md) abstraction reads and writes records by grain identity; a grain or application service can access a query-oriented database model directly. + +Use continuation tokens for resumable paging. A token can carry the routing version, shard or partition, last ordering value, and last grain key. Treat tokens as opaque application contracts and version their encoded form. + +Define whether paging observes a query snapshot or a changing data set. When the query store supports snapshots, include its snapshot identifier or read timestamp in the token. Otherwise, document how concurrent updates can produce repeated or omitted items and make page processing idempotent. Validate or authenticate client-supplied tokens before using their routing or ordering fields. + +[Response streaming](../grains/response-streaming.md) progressively delivers one live query result with pull-based flow control. A page with a continuation token provides an explicit resume boundary which a caller can persist before cancellation, timeout, grain deactivation, or process failure. + +## Examples + +- The [Journaled Todo List registry grain](https://github.com/dotnet/orleans/blob/main/samples/JournaledTodoList/JournaledTodoList.WebApp/Grains/TodoListRegistryGrain.cs) maintains a bounded catalog of list identities and display names. +- The [Shopping Cart inventory grains](https://github.com/dotnet/orleans/blob/main/samples/ShoppingCart/Grains/InventoryGrain.cs) partition product membership by category, and the [inventory service](https://github.com/dotnet/orleans/blob/main/samples/ShoppingCart/Silo/Services/InventoryService.cs) merges the known partitions. +- The [Azure App Service inventory grain](https://github.com/dotnet/orleans/blob/main/samples/Deployment/AzureAppService/Grains/InventoryGrain.cs) returns inventory incrementally using response streaming. diff --git a/docs/site/src/content/docs/toc.yml b/docs/site/src/content/docs/toc.yml index 6c7037e1fe1..8caa4252f12 100644 --- a/docs/site/src/content/docs/toc.yml +++ b/docs/site/src/content/docs/toc.yml @@ -202,6 +202,8 @@ items: href: how-to/index.md - name: Implement long-running reminders href: how-to/long-running-reminders.md + - name: Model collections of grains + href: how-to/model-grain-collections.md - name: Host and configure items: - name: Hosting