From f45655bc8681cc31b4b42de65efe25da442c54d3 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Fri, 21 Aug 2026 00:35:45 -0700 Subject: [PATCH 1/5] docs(grains): document distributed collection patterns --- .../docs/grains/grain-persistence/index.md | 4 +- docs/site/src/content/docs/grains/index.md | 1 + .../docs/grains/model-grain-collections.md | 79 +++++++++++++++++++ docs/site/src/content/docs/toc.yml | 2 + 4 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 docs/site/src/content/docs/grains/model-grain-collections.md 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..b63e730c552 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](../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..baf888f47e1 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](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/grains/model-grain-collections.md b/docs/site/src/content/docs/grains/model-grain-collections.md new file mode 100644 index 00000000000..ec86159a927 --- /dev/null +++ b/docs/site/src/content/docs/grains/model-grain-collections.md @@ -0,0 +1,79 @@ +--- +title: Model collections of grains +description: Model bounded catalogs, partitioned indexes, query stores, paging, and bulk operations over Orleans grains. +ms.date: 08/21/2026 +ms.topic: concept-article +--- + +# 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. The runtime grain directory tracks active grain locations for request routing; 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](grain-references.md) from those keys when invoking members. Persist a grain reference when retaining its interface relationship is useful. + +## 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, and one awaited persistent state write provides an optimistic-concurrency boundary. + +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. + +## 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 awaited state write durably records both changes | +| Member and index use Orleans transactional state | An [Orleans transaction](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](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. + +[Response streaming](response-streaming.md) progressively delivers one live query result with pull-based flow control. A page with a continuation token provides a durable resume boundary when a caller must continue after cancellation, timeout, grain deactivation, or process failure. + +## Maintained 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..a1b3b50d902 100644 --- a/docs/site/src/content/docs/toc.yml +++ b/docs/site/src/content/docs/toc.yml @@ -26,6 +26,8 @@ items: href: grains/grain-references.md - name: Grain identity href: grains/grain-identity.md + - name: Model collections of grains + href: grains/model-grain-collections.md - name: Grain activation and lifecycle href: grains/grain-lifecycle.md - name: Grain extensions From 7b57cea1a005f8a49647df2a5b6056d0ad9ce962 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Fri, 21 Aug 2026 03:09:52 -0700 Subject: [PATCH 2/5] docs(grains): clarify collection member models --- .../site/src/content/docs/grains/model-grain-collections.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/site/src/content/docs/grains/model-grain-collections.md b/docs/site/src/content/docs/grains/model-grain-collections.md index ec86159a927..ab098caad24 100644 --- a/docs/site/src/content/docs/grains/model-grain-collections.md +++ b/docs/site/src/content/docs/grains/model-grain-collections.md @@ -24,6 +24,12 @@ A grain key identifies a logical grain, and Date: Fri, 21 Aug 2026 08:45:56 -0700 Subject: [PATCH 3/5] docs(grains): remove maintained wording --- docs/site/src/content/docs/grains/model-grain-collections.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/site/src/content/docs/grains/model-grain-collections.md b/docs/site/src/content/docs/grains/model-grain-collections.md index ab098caad24..7181784fb44 100644 --- a/docs/site/src/content/docs/grains/model-grain-collections.md +++ b/docs/site/src/content/docs/grains/model-grain-collections.md @@ -78,7 +78,7 @@ Use continuation tokens for resumable paging. A token can carry the routing vers [Response streaming](response-streaming.md) progressively delivers one live query result with pull-based flow control. A page with a continuation token provides a durable resume boundary when a caller must continue after cancellation, timeout, grain deactivation, or process failure. -## Maintained examples +## 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. From 76f0b424de8a04e990b30fa271cb9bab497d61f6 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Fri, 21 Aug 2026 09:54:34 -0700 Subject: [PATCH 4/5] docs(grains): classify collection guidance as how-to --- .../src/content/docs/grains/grain-persistence/index.md | 2 +- docs/site/src/content/docs/grains/index.md | 2 +- docs/site/src/content/docs/how-to/index.md | 1 + .../docs/{grains => how-to}/model-grain-collections.md | 10 +++++----- docs/site/src/content/docs/toc.yml | 4 ++-- 5 files changed, 10 insertions(+), 9 deletions(-) rename docs/site/src/content/docs/{grains => how-to}/model-grain-collections.md (89%) 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 b63e730c552..b3f6e019d97 100644 --- a/docs/site/src/content/docs/grains/grain-persistence/index.md +++ b/docs/site/src/content/docs/grains/grain-persistence/index.md @@ -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. See [Model collections of grains](../model-grain-collections.md) for registry, partitioned-index, and external query-store patterns. +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 baf888f47e1..cd01a06a682 100644 --- a/docs/site/src/content/docs/grains/index.md +++ b/docs/site/src/content/docs/grains/index.md @@ -87,7 +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](model-grain-collections.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/grains/model-grain-collections.md b/docs/site/src/content/docs/how-to/model-grain-collections.md similarity index 89% rename from docs/site/src/content/docs/grains/model-grain-collections.md rename to docs/site/src/content/docs/how-to/model-grain-collections.md index 7181784fb44..a37011a8986 100644 --- a/docs/site/src/content/docs/grains/model-grain-collections.md +++ b/docs/site/src/content/docs/how-to/model-grain-collections.md @@ -2,7 +2,7 @@ title: Model collections of grains description: Model bounded catalogs, partitioned indexes, query stores, paging, and bulk operations over Orleans grains. ms.date: 08/21/2026 -ms.topic: concept-article +ms.topic: how-to --- # Model collections of grains @@ -22,7 +22,7 @@ A grain key identifies a logical grain, and 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](grain-references.md) from those keys when invoking members. Persist a grain reference when retaining its interface relationship is useful. +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 @@ -62,7 +62,7 @@ Collection membership and member state often occupy different records. Choose th | Update model | Completion contract | |---|---| | Member data and membership share one grain state record | The awaited state write durably records both changes | -| Member and index use Orleans transactional state | An [Orleans transaction](transactions.md) commits both changes atomically | +| 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 | @@ -72,11 +72,11 @@ For asynchronous indexes, retain enough information to replay updates and reconc ## 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](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 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. -[Response streaming](response-streaming.md) progressively delivers one live query result with pull-based flow control. A page with a continuation token provides a durable resume boundary when a caller must continue after cancellation, timeout, grain deactivation, or process failure. +[Response streaming](../grains/response-streaming.md) progressively delivers one live query result with pull-based flow control. A page with a continuation token provides a durable resume boundary when a caller must continue after cancellation, timeout, grain deactivation, or process failure. ## Examples diff --git a/docs/site/src/content/docs/toc.yml b/docs/site/src/content/docs/toc.yml index a1b3b50d902..8caa4252f12 100644 --- a/docs/site/src/content/docs/toc.yml +++ b/docs/site/src/content/docs/toc.yml @@ -26,8 +26,6 @@ items: href: grains/grain-references.md - name: Grain identity href: grains/grain-identity.md - - name: Model collections of grains - href: grains/model-grain-collections.md - name: Grain activation and lifecycle href: grains/grain-lifecycle.md - name: Grain extensions @@ -204,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 From bc627016d543c6c512f701d83f002fe1c9a38f33 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Tue, 25 Aug 2026 03:15:27 -0700 Subject: [PATCH 5/5] docs(grains): clarify collection guarantees --- .../content/docs/how-to/model-grain-collections.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) 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 index a37011a8986..3a4cda7495c 100644 --- a/docs/site/src/content/docs/how-to/model-grain-collections.md +++ b/docs/site/src/content/docs/how-to/model-grain-collections.md @@ -1,7 +1,7 @@ --- title: Model collections of grains description: Model bounded catalogs, partitioned indexes, query stores, paging, and bulk operations over Orleans grains. -ms.date: 08/21/2026 +ms.date: 08/25/2026 ms.topic: how-to --- @@ -9,7 +9,7 @@ ms.topic: how-to 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. The runtime grain directory tracks active grain locations for request routing; application catalogs retain membership across deactivation and cluster restarts. +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 @@ -32,7 +32,7 @@ Store ordinary serializable objects in an owning grain's state when they form a ## 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, and one awaited persistent state write provides an optimistic-concurrency boundary. +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: @@ -55,13 +55,15 @@ Each membership entry has one owning shard. Keep the shard count and hash algori 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 awaited state write durably records both changes | +| 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 | @@ -76,7 +78,9 @@ Use a database or search service as the query authority when the workload needs 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. -[Response streaming](../grains/response-streaming.md) progressively delivers one live query result with pull-based flow control. A page with a continuation token provides a durable resume boundary when a caller must continue after cancellation, timeout, grain deactivation, or process failure. +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