HistoryViews 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 HistoryViews sample.
Program.cs
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Orleans.Hosting;
using Orleans.Lattice;
using Orleans.Lattice.Views;
// HistoryViews sample
// ===================
// Demonstrates a DURABLE per-key history view: an opt-in, append-only
// materialised view that records every revision of every key in a source tree.
// Unlike the best-effort WAL-window fallback (see the ChangeHistory sample), the
// history view survives source WAL garbage collection and is bounded only by its
// configured retention age, so ScanEntryHistoryAsync reports Source == View and
// never truncates the timeline below.
//
// Two pieces of setup make this work:
// 1. AddLatticeViews() on the silo (the view catalog + maintainer).
// 2. A runtime view created via ILatticeViewFactory using
// LatticeHistoryView.Definition, plus SetHistoryRetentionAsync to keep the
// LWW value bytes verbatim (FullValue) so we can read old values back.
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));
// The view subsystem: catalog, factory, and maintainer. Required for a
// durable history view.
silo.AddLatticeViews();
})
.Build();
Console.Write("Silo starting...");
await host.StartAsync();
Console.WriteLine(" ready.");
Console.WriteLine();
var grains = host.Services.GetRequiredService<IGrainFactory>();
var factory = host.Services.GetRequiredService<ILatticeViewFactory>();
var orders = grains.GetGrain<ILattice>("orders");
const string key = "order-42";
// Keep full value bytes for every revision (no age bound) so the durable
// timeline can serve point-in-time values directly.
// Pass null for the window to clear any age bound (revisions never expire);
// the setter rejects TimeSpan.Zero - use null to mean "no age bound".
await orders.SetHistoryRetentionAsync(
HistoryRetentionMode.FullValue,
window: null,
CancellationToken.None);
var retention = await orders.GetHistoryRetentionAsync();
Console.WriteLine($"History retention for 'orders': mode={retention.Mode}, window={retention.Window}");
// Enable history: a runtime view named "orders-history" tailing "orders".
// History is forward-only, so create it BEFORE writing the revisions we want kept.
var history = factory.Create(
orders,
"orders-history",
LatticeHistoryView.Definition("orders-history", host.Services));
Console.WriteLine("Durable history view 'orders-history' created (forward-only).");
Console.WriteLine();
// Write a sequence of values to ONE key. Each Set is a durable revision.
string[] lifecycle = ["placed", "paid", "packed", "shipped", "delivered"];
Console.WriteLine($"Writing {lifecycle.Length} successive revisions to key '{key}':");
foreach (var status in lifecycle)
{
await orders.SetAsync(key, Encoding.UTF8.GetBytes(status));
Console.WriteLine($" set '{key}' = '{status}'");
}
Console.WriteLine();
// The view is eventually consistent: wait (bounded) for the maintainer to apply
// every source write so the sample output is deterministic.
await history.WaitForSourceHeadAsync(TimeSpan.FromSeconds(30), CancellationToken.None);
Console.WriteLine($"View apply lag after catch-up: {await history.GetLagAsync()}");
Console.WriteLine();
// A plain read collapses to the latest value - the older revisions are gone from
// the live tree. The durable history view is what preserves them.
var current = await orders.GetAsync(key);
Console.WriteLine($"Plain GetAsync('{key}') -> '{Encoding.UTF8.GetString(current!)}' (latest only)");
Console.WriteLine();
// Read the durable timeline. FullValue retention means each revision carries its
// value bytes in ValuePreview, so we can print the historical value verbatim.
Console.WriteLine($"ScanEntryHistoryAsync('{key}') - the durable revision timeline:");
var revisionNumber = 0;
string? continuation = null;
EntryHistoryPage page;
do
{
page = await orders.ScanEntryHistoryAsync(
key,
fromHlc: null,
toHlc: null,
limit: 100,
continuation: continuation,
CancellationToken.None);
foreach (var revision in page.Revisions)
{
revisionNumber++;
var value = revision.ValuePreview is null
? "<metadata-only>"
: Encoding.UTF8.GetString(revision.ValuePreview);
Console.WriteLine(
$" #{revisionNumber} hlc={revision.Hlc} kind={revision.Kind} " +
$"value='{value}' (shape={revision.RetentionShape})");
}
continuation = page.Continuation;
}
while (continuation is not null);
Console.WriteLine();
Console.WriteLine($"Source={page.Source} (View = durable history view, survives WAL GC)");
Console.WriteLine($"Truncated={page.Truncated} (always false on the View path - bounded only by retention age)");
Console.WriteLine($"Total durable revisions read: {revisionNumber}");
await host.StopAsync();
HistoryViews.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Orleans.Lattice.Samples.HistoryViews</RootNamespace>
<AssemblyName>Orleans.Lattice.Samples.HistoryViews</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>