How to Query Objects Without Scanning Every Record

Loading every record and then applying a filter is an easy way to make a small application work. It is also an easy way to hide the cost of a lookup. If the user asks for one SKU or all products in a category, the application already knows which field determines the answer. That is a useful place to declare an index instead of turning every question into a full record scan.

Sphere10 Framework's ObjectSpace supports identifier, unique and nonunique member indexes. The distinction matters because a SKU resolves to one saved position, while a category can resolve to a set of positions. This tutorial demonstrates both questions using a three-product catalogue and checks an absent SKU. It deliberately uses supported indexed lookups rather than suggesting that arbitrary LINQ predicates are automatically translated into a storage query.

A focused product lookup within an indexed catalogue.
A focused product lookup within an indexed catalogue.

Contents

Prepare the Example

Use the .NET 10 SDK and Sphere10 Framework 3.1.3. In a new console project, add the core package:


dotnet new console --framework net10.0 --name ObjectSpaceQueries
cd ObjectSpaceQueries
dotnet add package Sphere10.Framework --version 3.1.3

Replace the generated source with the named files below, or download the complete example at the end. The package supplies the storage, serialization and collection mechanisms, while the small model and workflow belong to this application. This example uses an isolated in-memory dataset, so running it does not alter an existing application store.

Declare the Model and the Questions

Use the following Product.cs:


namespace ArticleSample;

public sealed class Product {
	public long Id { get; set; }
	public string Sku { get; set; }
	public string Name { get; set; }
	public int Category { get; set; }
	public decimal Price { get; set; }
}

The important part of the schema is not the number of properties. The application contract gives each property a distinct role. Id identifies the record, Sku must select at most one record, and Category groups records. A separate index has a storage and maintenance cost, so index fields that support real operations rather than adding every possible index by default.

The fluent configuration names each of those roles. A member-expression lookup such as Get((Product item) => item.Sku, "KB-101") uses a configured unique or identifier index. It is not an invitation to replace the member expression with a complex business predicate and expect the storage layer to plan a query.

Follow the Matching Positions Back to Objects

Save this complete example as Program.cs:


using System;
using System.Linq;
using Sphere10.Framework;
using Sphere10.Framework.ObjectSpaces;

namespace ArticleSample;

public static class Program {
	public static void Main() {
		var builder = new ObjectSpaceBuilder().UseMemoryStream();
		builder.AddDimension<Product>(ignoreAnnotations: true)
			.WithIdentifier(item => item.Id)
			.WithUniqueIndexOn(item => item.Sku)
			.WithIndexOn(item => item.Category)
			.Done();
		using var space = builder.Build();
		space.Save(new Product { Id = 101, Sku = "KB-101", Name = "Keyboard", Category = 7, Price = 89m });
		space.Save(new Product { Id = 102, Sku = "MS-102", Name = "Mouse", Category = 7, Price = 65m });
		space.Save(new Product { Id = 103, Sku = "MN-103", Name = "Monitor", Category = 9, Price = 249m });
		var keyboard = space.Get((Product item) => item.Sku, "KB-101");
		var accessories = InCategory(space, 7);
		var displays = InCategory(space, 9);
		var missing = space.TryGet((Product item) => item.Sku, "UNKNOWN", out var ignored);
		if (accessories.Length != 2 || displays.Length != 1 || displays[0].Sku != "MN-103" || missing)
			throw new InvalidOperationException("Indexed lookup returned unexpected data.");
		Console.WriteLine($"Unique SKU lookup: {keyboard.Name}");
		Console.WriteLine($"Category 7 products: {accessories.Length}");
		Console.WriteLine($"Category 9 products: {displays.Length}");
		Console.WriteLine($"Unknown SKU found: {missing}");
	}

	private static Product[] InCategory(ObjectSpace space, int category) {
		using (space.EnterAccessScope()) {
			var dimension = (ObjectSpace.Dimension<Product>)space.Dimensions.Single(item => item.Definition.ObjectType == typeof(Product));
			var positions = dimension.Container.ObjectStream.GetIndexFor(item => item.Category).Values[category].ToArray();
			return positions.Select(space.Get<Product>).OrderBy(item => item.Id).ToArray();
		}
	}
}

The unique path is short because ObjectSpace resolves the configured SKU key and returns the product through its identity tracker. For the nonunique path, the example obtains the Product dimension, selects its category index and materializes the matching positions while holding the access scope. It then calls space.Get<Product> for each position. Those final reads preserve the store's tracked-object identity instead of bypassing it through a raw object container.

Notice the generic Dimensions.Single(...) enumeration. It selects the dimension by its registered object type. The returned objects are ordered by business ID for predictable presentation. That sort operates only on the matched results. It is not a claim that the category index sorts the whole catalogue by product ID.

Check Found, Grouped and Missing Results

Run the sample:


dotnet run -c Release

The expected output is:


Unique SKU lookup: Keyboard
Category 7 products: 2
Category 9 products: 1
Unknown SKU found: False

The two category checks are useful acceptance tests. A result of two accessories and one display proves more than printing a product name because it checks the cardinality of a nonunique index and the relationship between matching positions and materialized records. The absent SKU uses TryGet, making an expected miss a normal result rather than treating every unsuccessful search as an exception.

💡 Materialize an index's positions inside the store's access scope, then use ObjectSpace retrieval for records that will participate in its identity and save/delete behavior. Avoid retaining a lazy index enumeration across later mutations.

Understand What This Example Does Not Query

This is an insertion-and-lookup example. It does not demonstrate changing indexed keys, an SQL join planner or a general query language. Those are separate behavior paths and should have their own tests against the selected Framework version. An application can combine multiple indexed questions, but it owns the logic for intersecting result sets, applying additional predicates and selecting a presentation order.

For a wide category with many matching records, the index cannot eliminate the cost of returning the requested records. Measure the selectivity and the number of objects materialized for the actual workload. Likewise, a file-backed session adds paging, I/O and transaction decisions that this in-memory example intentionally leaves out. There is no measured speedup figure attached to this tutorial.

Treat the model, serializer and indexes as a versioned schema when moving the example to persistent storage. Reopening an existing file with a different index layout is not a safe way to add a feature. Preserve its original specification or perform an explicit, tested application migration.

Continue Building

The complete sample project includes the project file, source, expected console output and MIT license notice. Code excerpts on this page are supplied under that same license. Keep the package version with your application and retain the sample's observable checks when adapting it because compilation alone does not prove persistence or correct query results.

sample.zip
Download the complete worked example and build instructions (ZIP).

Sphere10 Framework lets you compose this capability with the rest of your application rather than adopt an unrelated application shell. Start with the working example, change one schema or workload decision at a time, and verify the resulting behavior with your own records.

Continue with the file-backed catalogue to make these queries survive a process restart, or the transaction tutorial to separate accepted edits from pending work.

Useful references:

Continue the Series

How to Build an Embedded Object Database in C#

How to Give Your C# Objects Commit and Rollback