What Is Self-Describing Storage?

A binary store is only readable by code that knows how it was written. For an ObjectSpace that knowledge is the serializer layout of every type, the order of the dimensions and the specification of every index. Normally the application supplies all of it again each time it opens the file, and a tool that lacks the original definition cannot read the data.

Self-describing storage keeps that construction metadata inside the store. Sphere10 Framework can then rebuild the configuration from the bytes and open an ObjectSpace or ObjectStream without a supplied definition. This article explains what is recorded, where it lives, how reopening works and what the metadata does not cover.

A portable data container retaining the metadata needed to reopen its records.
A portable data container retaining the metadata needed to reopen its records.
💡 In short. What self-describing storage records, where the metadata lives in the file, how ObjectSpace.TryOpenFromStream rebuilds a store without a supplied definition, and what the metadata does not cover.

1. A first look

One call on the builder, StoreMetaData(), turns the option on when a store is created.

C#
using var raw = new MemoryStream();
var builder = new ObjectSpaceBuilder()
	.UseMemoryStream(raw)
	.WithEndianness(Endianness.LittleEndian)
	.StoreMetaData();
using (var created = SampleSchema.Configure(builder).Build()) {
	created.Save(new Customer { Id = 1, Email = "[email protected]", Region = 7, Name = "Ana" });
	created.Save(new Purchase { Id = 100, Reference = "PO-0100", CustomerId = 1, Total = 125m });
	created.Save(new Purchase { Id = 101, Reference = "PO-0101", CustomerId = 1, Total = 40m });
	created.Flush();
}

SampleSchema.Configure adds the sample's two dimensions and their indexes in the usual way. Reopening the bytes later takes a stream, the byte order and an out parameter.

C#
received.Position = 0;
if (!ObjectSpace.TryOpenFromStream(received, Endianness.LittleEndian, out var reopened))
	throw new InvalidOperationException("The self-describing store could not be opened.");

The second call was given no builder and no dimension list, because the store carries its own definition. The sections that follow explain what is recorded, where it lives and how reopening works.

2. Supplied definition versus stored definition

There are two ways to open an existing store.

  • Supplied definition. The store is created with an ordinary builder and reopened with the same builder configuration. The schema knowledge lives in application code. This suits one application that owns its files.
  • Self-describing. The store is created with a builder that calls StoreMetaData() and reopened with ObjectSpace.TryOpenFromStream. The schema knowledge lives in the store, beside the data. This suits inspection tools, transfer between programs and long-lived files.

The supplied-definition path is the default and is what the embedded object database tutorial uses. It is a sound design when one application owns both the code and the data. Self-describing storage is an opt-in for cases where the reader should not need the writer's builder code.

3. Store the definition beside the records

Apart from the StoreMetaData() call shown in the first look, nothing about creating or using the store changes.

Two kinds of record are written. The ObjectSpace record is stored once and holds what all dimensions share. Each dimension's ObjectStream then stores its own record.

Record Contents
ObjectSpace The hash function and one serializer factory for all dimensions, including original type codes, member layouts and member order
ObjectStream, one per dimension The item type and its type code, the change-tracking member, and every index with its kind, name, member, null policy and B-tree type and order

Because the serializer factory itself is saved, reopening does not inspect today's class members and guess the old layout. It restores the layout that wrote the data.

4. Where the metadata lives

Every ObjectSpace and ObjectStream is a clustered-streams container with a fixed 256-byte header. When metadata is enabled, a flag in that header marks an allocation that follows it, and the header's last field records the allocation's capacity. The container's ordinary clusters begin after the allocation.

The capacity is the MetaDataSize field in the last four bytes of the header. The record begins at byte 256 with its own four-byte length, which counts the whole record, and zero padding fills the allocation up to the capacity.

StoreMetaData() allocates exactly the size of the record. StoreMetaData(capacity) reserves a larger budget in bytes, and a negative value is rejected. A store created without the option keeps the plain layout, with clusters starting at byte 256.

5. Open a store without its definition

ObjectSpace.TryOpenFromStream, the call that reopened the store in the first look, needs no builder and no dimension list. The space it returns answers lookups through the stored indexes.

C#
var customer = reopened.Get((Customer item) => item.Email, "[email protected]");
var purchases = IndexQueries.By(reopened, (Purchase item) => item.CustomerId, customer.Id);

The method reconstructs a normal builder configuration and then loads the store through the same path as any other open.

The result is an ordinary ObjectSpace. Identifier and unique lookups, nonunique index lookups, Save and Delete all behave as they do on a store opened from a definition. The contract of the call has four parts worth knowing.

  • Byte order is supplied by the caller. It is not detected from the bytes, and the wrong value makes the method return false.
  • false means no usable metadata. A store created without StoreMetaData(), an invalid header and a mismatched byte order all return false. I/O and deserializer exceptions still propagate.
  • Reading starts at the stream's current position. Set Position before the call.
  • The stream is borrowed. Dispose the returned space before disposing or reopening the stream.

A standalone ObjectStream<T> has the same facility through its storeMetaData: true constructor argument and ObjectStream.TryOpenFromStream.

6. Capacity does not imply migration

âš ī¸ Metadata describes how a store was constructed. It is not a migration mechanism, and several things remain outside it.
  • Existing stores are not upgraded. Enabling the option on a builder does not insert metadata into a file that was created without it. Such files keep opening through their supplied definition.
  • Spare capacity is only reserved space. It does not add dimensions, change serializer layouts or rebuild indexes in an existing store.
  • The types must still exist. The record names CLR types and serializer types. The reading process needs compatible Framework code and assemblies that resolve those names.
  • Some configuration cannot be recorded. Custom comparer registrations, delegate-based change tracking and serializers built from captured delegates have no stored form, so creation rejects them when metadata is requested.
  • A raw open is not transactional. Opening a committed file as a stream does not recreate the FileObjectSpace transaction wrapper.
â„šī¸ Metadata answers how to read the bytes. Whether the content is a version your application supports is a separate question. Keep your own version number in a stored record if you need to answer it.

7. Get the package and run the sample

Add the package to a .NET 10 project.

Bash
dotnet add package Sphere10.Framework

The complete MIT-licensed sample creates a self-describing store, writes it to a file, reopens the bytes without a definition and reads records through their indexes. It also confirms that the wrong byte order and a store without metadata are both refused. It targets .NET 10 and the Sphere10.Framework package.

Bash
dotnet run -c Release -- ./fresh-data
Plain text
Opened without a definition: 2 dimensions, 2 purchases for [email protected]
Wrong byte order refused: passed
Store without metadata refused: passed

The metadata format reference gives the exact byte layout, and the self-describing store tutorial builds a store step by step.


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

For the broader Sphere 10 development toolkit, see the Sphere10 Framework overview .

Author: Herman Schoenfeld