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

Part of [Strongly Consistent Scans](README.md).

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

## Program.cs

````csharp
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Orleans.Hosting;
using Orleans.Lattice;

// =============================================================================
// StronglyConsistentScans
// -----------------------------------------------------------------------------
// Demonstrates that CountAsync / ScanKeysAsync / ScanEntriesAsync count every
// live key exactly once - none missed or double-counted because a shard split
// or rebalanced underneath the call, and a concurrent SetManyAtomicAsync seen
// all-or-nothing - even while foreground writes are landing concurrently.
//
// Why this matters: in many sharded stores an aggregate like "count" is a
// best-effort fan-out that can miss or double-count keys a concurrent split or
// rebalance moves between shards. Lattice scans are strongly consistent, but
// each shard is read at its own moment, so a reading taken while ordinary
// writes land is exact shard by shard rather than one instant's image of the
// whole tree. For an add-only stream like this sample's, that still makes
// every reading a count the tree genuinely held at some instant, and a stream
// of readings is monotonic.
// =============================================================================

// Single-silo in-process Orleans cluster with Lattice on in-memory storage.
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));
    })
    .Build();

Console.Write("Silo starting...");
await host.StartAsync();
Console.WriteLine(" ready.");
Console.WriteLine();

var grainFactory = host.Services.GetRequiredService<IGrainFactory>();
var tree = grainFactory.GetGrain<ILattice>("strongly-consistent-scans");

static byte[] Value(string s) => Encoding.UTF8.GetBytes(s);

// --- Step 1: seed a known baseline -----------------------------------------
// Write 500 keys up front. After this completes, the tree holds exactly 500
// live keys and every scan primitive must agree on that number.
const int seedCount = 500;
Console.WriteLine($"Seeding {seedCount} keys (item:0000 .. item:0499)...");
for (var i = 0; i < seedCount; i++)
{
    await tree.SetAsync($"item:{i:D4}", Value($"v{i}"));
}

var baselineCount = await tree.CountAsync();
var baselineScan = 0;
await foreach (var _ in tree.ScanKeysAsync())
{
    baselineScan++;
}

Console.WriteLine($"  CountAsync()      = {baselineCount}");
Console.WriteLine($"  ScanKeysAsync -> {baselineScan} keys");
Console.WriteLine($"  Agree on baseline: {baselineCount == seedCount && baselineScan == seedCount}");
Console.WriteLine();

// --- Step 2: hammer the tree with concurrent writes while we count ---------
// A background writer adds 300 more keys, one at a time, with a tiny pause so
// the writes genuinely interleave with our reads. Meanwhile the foreground
// loop calls CountAsync as fast as it can and records every value it sees.
const int extraCount = 300;
const int finalTotal = seedCount + extraCount;

Console.WriteLine($"Adding {extraCount} keys (extra:0000 .. extra:0299) concurrently while counting...");

var writer = Task.Run(async () =>
{
    for (var i = 0; i < extraCount; i++)
    {
        await tree.SetAsync($"extra:{i:D4}", Value($"x{i}"));
        await Task.Delay(1);
    }
});

var readings = new List<int>();
while (!writer.IsCompleted)
{
    readings.Add(await tree.CountAsync());
}
await writer;

// Every reading is a real committed count, so the sequence can only climb:
// it starts at or above the baseline, ends at or below the final total, and
// never goes backwards. A partial/torn fan-out would break one of these.
var minReading = readings.Count > 0 ? readings.Min() : baselineCount;
var maxReading = readings.Count > 0 ? readings.Max() : baselineCount;
var monotonic = true;
for (var i = 1; i < readings.Count; i++)
{
    if (readings[i] < readings[i - 1])
    {
        monotonic = false;
        break;
    }
}

Console.WriteLine($"  All observed counts stayed within [{seedCount}, {finalTotal}]: " +
    $"{minReading >= seedCount && maxReading <= finalTotal}");
Console.WriteLine($"  Observed counts were monotonic (never went backwards): {monotonic}");
Console.WriteLine();

// --- Step 3: confirm the settled state is exact -----------------------------
// After the writer drains, the tree holds exactly 800 keys. CountAsync and a
// full ScanEntriesAsync must both report that number, with no duplicate keys.
var finalCount = await tree.CountAsync();
var seen = new HashSet<string>();
await foreach (var entry in tree.ScanEntriesAsync())
{
    seen.Add(entry.Key);
}

Console.WriteLine("Settled state after concurrent writes:");
Console.WriteLine($"  CountAsync()             = {finalCount}");
Console.WriteLine($"  Distinct keys from scan  = {seen.Count}");
Console.WriteLine($"  Exact and duplicate-free: {finalCount == finalTotal && seen.Count == finalTotal}");
Console.WriteLine();

Console.WriteLine("Done: scans returned the exact live key set throughout concurrent writes.");

await host.StopAsync();
````

## StronglyConsistentScans.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.StronglyConsistentScans</RootNamespace>
    <AssemblyName>Orleans.Lattice.Samples.StronglyConsistentScans</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" />
  </ItemGroup>

</Project>
````
