Scan keys and query labeled entities
In this tutorial, you store products as labeled entities, list them by key prefix with a scan, and then run indexed queries that filter, order, and limit results — without reading every key yourself. Scanning walks a key range; querying uses the labels you attach at write time to answer questions like "cheap books, most expensive first." You need the .NET 8 SDK or later and a text editor.
Scanning and querying both live on IZaris.Scan, so you cast the client to IZaris first. Labels are the key: a value you tag with indexed labels becomes searchable by those labels through a SearchQuery.
1. Create a project and add the SDK
Create a console project and add the Zaris SDK.
dotnet new console -o zaris-scan-query
cd zaris-scan-query
dotnet add package Clustron.Zaris.SDK
2. Get an IZaris client
In Program.cs, register an in-process store and resolve the client as IZaris so the Scan accessor is available.
using Clustron.Zaris.Abstractions;
using Clustron.Zaris.Client;
using Clustron.Zaris.Client.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection()
.AddClustronZaris("demo", "zaris://inproc/demo")
.BuildServiceProvider();
var client = (IZaris)await services
.GetRequiredService<IZarisClientProvider>()
.GetAsync("demo");
3. Seed labeled entities
Store each product with Put.WithEntity to group it under an entity name, and WithLabel to attach the fields you want to query on. Labels are indexed by default, so category, name, and price become searchable. Give the price as a numeric string so range queries can compare it.
First define the entity type. In a top-level Program.cs, put this record at the bottom of the file, after your executable statements.
record Product(string Id, string Name);
Then seed the catalog.
var catalog = new (string Key, string Name, string Category, int Price)[]
{
("product:1", "Clean Code", "books", 39),
("product:2", "The Pragmatic Programmer", "books", 45),
("product:3", "Mechanical Keyboard", "electronics", 120),
("product:4", "USB-C Cable", "electronics", 12),
("product:5", "Refactoring", "books", 55),
};
foreach (var p in catalog)
{
await client.PutAsync(
p.Key,
new Product(p.Key, p.Name),
Put.WithEntity("product")
.WithLabel("category", p.Category)
.WithLabel("name", p.Name)
.WithLabel("price", p.Price.ToString()));
}
4. List keys with a prefix scan
Use Scan.ScanAsync with a KeyRange to stream keys. Set Prefix to walk every key under product:. ScanAsync yields a KvResult<string> per key, and the key itself is on .Value.
Console.WriteLine("All product keys:");
await foreach (var entry in client.Scan.ScanAsync(new KeyRange { Prefix = "product:" }))
{
Console.WriteLine($" {entry.Value}");
}
A scan does not deserialize values; it is the fast way to enumerate the keys in a range. When you want the objects behind those keys filtered by their fields, use a query instead.
5. Query with a single clause
Build a SearchQuery with SearchQuery.For(entity) and add clauses fluently. Run it with Scan.SearchAsync, then materialize the matches as your type with AsEntities<T>(), which returns a reader you advance with ReadAsync and read from Current. This query returns every product labeled category = books.
var booksQuery = SearchQuery.For("product").Eq("category", "books");
await using var reader = await (await client.Scan.SearchAsync(booksQuery)).AsEntities<Product>();
Console.WriteLine("Books:");
while (await reader.ReadAsync())
{
Console.WriteLine($" {reader.Current.Name}");
}
Eq matches a label exactly. SearchAsync returns a result builder, and AsEntities<T>() deserializes each matching value into a Product.
6. Combine clauses, order, and limit
Clauses compose. Use Range for numeric labels, And to require several conditions, Not to exclude, OrderBy to sort, and Limit to cap the result count. This query finds books priced between 10 and 50, excludes one title, orders by price ascending, and takes the first two.
var query = SearchQuery.For("product")
.And(
new EqClause("category", "books"),
new RangeClause("price", 10, 50))
.Not(new EqClause("name", "Clean Code"))
.OrderBy("price", ascending: true)
.Limit(2);
await using var results = await (await client.Scan.SearchAsync(query)).AsEntities<Product>();
Console.WriteLine("Cheap books (filtered, ordered, limited):");
while (await results.ReadAsync())
{
Console.WriteLine($" {results.Current.Name}");
}
Range bounds are inclusive and compare the label as a number. And and Not take clause objects such as EqClause and RangeClause, OrderBy sorts on a label with ascending controlling direction, and Limit caps the total returned across the whole result — not per page.
7. Page large result sets
For big queries, pass SearchOptions with a PageSize to control how many rows the reader fetches per round trip. The reader streams transparently across pages, so your while (await reader.ReadAsync()) loop does not change.
var options = new SearchOptions { PageSize = 100 };
var allProducts = SearchQuery.For("product").Range("price", 0, 1_000_000);
await using var paged = await (await client.Scan.SearchAsync(allProducts, options)).AsEntities<Product>();
var count = 0;
while (await paged.ReadAsync())
count++;
Console.WriteLine($"Total products: {count}");
What you built
You built a small catalog you can both enumerate and query. Writing values with Put.WithEntity and WithLabel made them searchable; Scan.ScanAsync(new KeyRange { Prefix = ... }) streamed the keys (each on the result's .Value); and Scan.SearchAsync(...) with AsEntities<T>() returned deserialized objects. You filtered with Eq and Range, composed conditions with And and Not, sorted with OrderBy, capped results with Limit, and paged large result sets with SearchOptions.PageSize. Reach for a scan when you need keys in a range, and a query when you need the objects behind them filtered by their labels.