GrainIndex source
This page is part of the documentation for Orleans.Lattice 9.9.0 (release line 9.9), built 2026-10-04. It is also published as markdown, with every table and list, at source.md, and llms.txt lists every page.The source of the GrainIndex sample.
Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Orleans.Hosting;
using Orleans.Lattice;
using Orleans.Lattice.GrainIndex;
using Orleans.Lattice.Samples.GrainIndex;
// GrainIndex sample
// =================
// A grain index tracks a grain's typed state in a lattice tree so you can ask
// "which User grains are 18 or over?" without hand-maintaining a secondary
// index and without activating every grain to find out.
//
// This sample:
// 1. Declares an index over IUserGrain's UserState, projecting Age and Country.
// 2. Writes a handful of users, each of which enrols itself on its write path.
// 3. Runs typed predicate queries against the index.
// 4. Shows a conjunction (Age >= 18 && Country == "UK"), which the planner
// turns into two range scans whose grain keys are intersected.
using var host = Host.CreateDefaultBuilder(args)
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.SetMinimumLevel(LogLevel.None);
})
.UseOrleans(silo =>
{
silo.UseLocalhostClustering();
silo.AddMemoryGrainStorageAsDefault();
silo.UseInMemoryReminderService();
silo.AddLattice((s, name) => s.AddMemoryGrainStorage(name));
// Declare the index. Only the properties named here are projected, so
// the write amplification of an index is always a deliberate choice.
silo.AddGrainIndex<IUserGrain, UserState>(index => index
.WithName("users")
.Include(u => u.Age)
.Include(u => u.Country));
})
.Build();
await host.StartAsync();
var grains = host.Services.GetRequiredService<IGrainFactory>();
var indexes = host.Services.GetRequiredService<IGrainIndexProvider>();
// 1. Write some users. Each WriteStateAsync re-projects that grain's entries.
var people = new (string Id, int Age, string Country)[]
{
("alice", 34, "UK"),
("bob", 17, "UK"),
("carla", 22, "IE"),
("dan", 61, "UK"),
("erin", 15, "IE"),
};
foreach (var (id, age, country) in people)
{
await grains.GetGrain<IUserGrain>(id).SetProfileAsync(age, country);
}
Console.WriteLine($"Wrote {people.Length} users.");
Console.WriteLine();
var index = indexes.GetIndex<IUserGrain, UserState>("users");
// 2. A single-property comparison becomes one contiguous range scan over the
// order-preserving key encoding - not a full scan plus a filter.
Console.WriteLine("Adults (Age >= 18):");
await foreach (var key in index.Where(u => u.Age >= 18).ToKeysAsync())
{
Console.WriteLine($" {key}");
}
Console.WriteLine();
// 3. Equality on a string property works the same way.
Console.WriteLine("Users in the UK:");
await foreach (var key in index.Where(u => u.Country == "UK").ToKeysAsync())
{
Console.WriteLine($" {key}");
}
Console.WriteLine();
// 4. A conjunction over two properties cannot be one predicate, because an
// index entry carries exactly one property. The planner issues one range
// scan per property and intersects the resulting grain keys.
Console.WriteLine("UK adults (Age >= 18 && Country == \"UK\"):");
await foreach (var grain in index
.Where(u => u.Age >= 18 && u.Country == "UK")
.ToGrainsAsync())
{
// The index is eventually consistent with respect to grain state, so
// confirm against the grain when the answer must be authoritative.
var age = await grain.GetAgeAsync();
Console.WriteLine($" {grain.GetPrimaryKeyString()} (age {age})");
}
Console.WriteLine();
// 5. A disjunction unions its branches and de-duplicates, so a grain matching
// both branches is yielded once.
var teenagersOrSeniors = await index
.Where(u => u.Age < 18 || u.Age >= 60)
.ToKeyListAsync();
Console.WriteLine($"Under 18 or 60+: {string.Join(", ", teenagersOrSeniors)}");
Console.WriteLine();
Console.WriteLine("Done.");
await host.StopAsync();
GrainIndex.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Orleans.Lattice.Samples.GrainIndex</RootNamespace>
<AssemblyName>Orleans.Lattice.Samples.GrainIndex</AssemblyName>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.11" />
<PackageReference Include="Microsoft.Orleans.Server" Version="10.2.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\lattice\Orleans.Lattice.csproj" />
<ProjectReference Include="..\..\src\lattice.grainindex\Orleans.Lattice.GrainIndex.csproj" />
</ItemGroup>
</Project>
UserGrain.cs
using Orleans.Lattice.GrainIndex;
using Orleans.Runtime;
namespace Orleans.Lattice.Samples.GrainIndex;
/// <summary>The typed state a <see cref="UserGrain"/> persists and the index projects.</summary>
[GenerateSerializer]
public sealed class UserState
{
/// <summary>The user's age, indexed so it can be range-scanned.</summary>
[Id(0)]
public int Age { get; set; }
/// <summary>The user's country, indexed so it can be matched for equality.</summary>
[Id(1)]
public string Country { get; set; } = string.Empty;
}
/// <summary>A user grain whose state is tracked in a grain index.</summary>
public interface IUserGrain : IGrainWithStringKey
{
/// <summary>Writes the user's profile, which re-projects its index entries.</summary>
/// <param name="age">The user's age.</param>
/// <param name="country">The user's country.</param>
Task SetProfileAsync(int age, string country);
/// <summary>Reads the user's age straight from grain state.</summary>
Task<int> GetAgeAsync();
}
/// <summary>
/// The grain implementation. <c>[Indexed]</c> stands in for
/// <c>[PersistentState]</c> and installs the index projection on the grain's
/// activation and write path, so no index maintenance appears in this code.
/// </summary>
/// <param name="state">The persistent, indexed state.</param>
public sealed class UserGrain([Indexed("user")] IPersistentState<UserState> state)
: IndexedGrain<UserState>(state), IUserGrain
{
/// <inheritdoc />
public async Task SetProfileAsync(int age, string country)
{
State.Age = age;
State.Country = country;
// WriteStateAsync persists the state AND republishes this grain's index
// entries as one atomic reconciliation.
await WriteStateAsync();
}
/// <inheritdoc />
public Task<int> GetAgeAsync() => Task.FromResult(State.Age);
}