---
title: "GrainIndex source"
url: "https://nsta1.github.io/Orleans.Lattice/samples/GrainIndex/source.html"
source: "https://github.com/NSTA1/Orleans.Lattice/tree/release/9.9/samples/GrainIndex"
documents: "Orleans.Lattice 9.9.0 (release line 9.9)"
built: "2026-10-04"
all-pages: "https://nsta1.github.io/Orleans.Lattice/llms.txt"
---
# GrainIndex source

Part of [Grain Index](README.md).

The source of the [GrainIndex](https://github.com/NSTA1/Orleans.Lattice/tree/release/9.9/samples/GrainIndex) sample.

## Program.cs

````csharp
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

````xml
<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

````csharp
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);
}
````
