CrystalData 0.48.0

dotnet add package CrystalData --version 0.48.0
                    
NuGet\Install-Package CrystalData -Version 0.48.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CrystalData" Version="0.48.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CrystalData" Version="0.48.0" />
                    
Directory.Packages.props
<PackageReference Include="CrystalData" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CrystalData --version 0.48.0
                    
#r "nuget: CrystalData, 0.48.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CrystalData@0.48.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CrystalData&version=0.48.0
                    
Install as a Cake Addin
#tool nuget:?package=CrystalData&version=0.48.0
                    
Install as a Cake Tool

CrystalData

NuGet Build and Test

CrystalData is a persistence engine for .NET. It combines snapshot files, optional journals, backups, and independently loaded storage nodes with Tinyhand serialization and ValueLink collections.

Contents

Requirements

  • .NET 10 SDK or later

Installation

dotnet add package CrystalData

Package Manager Console:

Install-Package CrystalData

Quick start

Define a Tinyhand-serializable data type:

using System.ComponentModel;
using Tinyhand;

[TinyhandObject]
public partial class FirstData
{
    [Key(0)]
    public int Id { get; set; }

    [Key(1)]
    [DefaultValue("Hoge")]
    public string Name { get; set; } = "Hoge";
}

Register the crystal, prepare the storage services, update the data, and shut down cleanly:

using CrystalData;
using Microsoft.Extensions.DependencyInjection;

var product = new CrystalUnit.Builder()
    .ConfigureCrystal(context =>
    {
        context.AddCrystal<FirstData>(
            new CrystalConfiguration(
                new LocalFileConfiguration("Local/FirstData.tinyhand"))
            {
                SaveFormat = SaveFormat.Utf8,
                NumberOfFileHistories = 1,
            });
    })
    .Build();

var services = product.Context.ServiceProvider;
var control = services.GetRequiredService<CrystalControl>();
var result = await control.PrepareAndLoad(useQuery: false);
if (result.IsFailure())
{
    throw new InvalidOperationException($"CrystalData initialization failed: {result}");
}

var data = services.GetRequiredData<FirstData>();
data.Id++;
data.Name = "Updated";

await control.StoreAndRip();

StoreAndRip is terminal: it stops new storage-point lock acquisitions as soon as shutdown begins. Stop application updates before calling it; if it fails, resolve the storage error and retry shutdown.

NativeAOT

CrystalData supports NativeAOT. Data types must use Tinyhand source generation and be registered through the generic AddCrystal<TData>, CreateCrystal<TData>, or GetOrCreateCrystal<TData> APIs so their closed generic forms are visible at build time.

Publish an application for a specific runtime identifier:

dotnet publish -c Release -r win-x64 --self-contained -p:PublishAot=true

The CrystalData project enables the .NET AOT and trimming compatibility analyzers, and CI publishes and runs QuickStart as a native executable.

Configuration

Each registered type has a CrystalConfiguration.

Property Purpose
SaveFormat Selects binary or UTF-8 Tinyhand output. The default comes from CrystalOptions.DefaultSaveFormat.
Volatile Keeps the crystal in memory without writing snapshot files.
SaveInterval Sets the automatic snapshot interval for the crystal.
NumberOfFileHistories Sets the number of retained snapshot files. Use 0 to disable file histories.
FileConfiguration Selects the primary snapshot file.
BackupFileConfiguration Selects an optional backup snapshot file.
StorageConfiguration Configures independently loaded StoragePoint<T> data.
RequiredForLoading Passes failures for previously stored data to the recovery query.

Global defaults and limits are configured with CrystalOptions:

context.SetCrystalOptions(new CrystalOptions
{
    GlobalDirectory = new LocalDirectoryConfiguration("Data"),
    DefaultBackup = new LocalDirectoryConfiguration("Backup"),
    DefaultSaveFormat = SaveFormat.Binary,
    MemoryUsageLimit = 512L * 1024 * 1024,
});

Register more than one crystal when an application has independently persisted data sets:

context.AddCrystal<FirstData>(
    new CrystalConfiguration(new GlobalFileConfiguration("First.tinyhand")));
context.AddCrystal<SecondData>(
    new CrystalConfiguration(new GlobalFileConfiguration("Second.tinyhand")));

CreateCrystal<TData> creates an independent instance on each call. GetOrCreateCrystal<TData> returns the registered instance for that type, or atomically creates and registers one. When an instance already exists, the supplied configuration is ignored. Use different paths for independent crystals.

Paths and backups

  • LocalFileConfiguration and LocalDirectoryConfiguration use absolute paths as-is. Relative paths are resolved against CrystalOptions.DataDirectory.
  • GlobalFileConfiguration and GlobalDirectoryConfiguration are resolved relative to CrystalOptions.GlobalDirectory.
  • EmptyFileConfiguration and EmptyDirectoryConfiguration disable the corresponding file or directory.
  • S3FileConfiguration and S3DirectoryConfiguration identify objects in an S3 bucket.

Set BackupFileConfiguration for one crystal, or set CrystalOptions.DefaultBackup to derive backup locations for crystals, journals, and auxiliary storage that do not define one explicitly.

Snapshot histories provide recovery candidates when the current file is missing or invalid. Journaling requires at least one history file for every journaled crystal.

Controls must not share writable files. Give each control its own DataDirectory, or explicitly separate all paths, including CrystalOptions.SupplementFile and BackupSupplementFile. Changing only GlobalDirectory does not relocate the default local supplement file.

Saving and shutdown

Use the lifecycle method that matches the operation:

Method Behavior
PrepareAndLoad() Prepares persistence services and loads registered crystals.
Store() Persists all managed crystals and auxiliary storage.
StoreAndRelease() Persists all managed data and attempts to release its resources.
StoreAndRip() Persists all managed data, records a clean shutdown, and terminates services.

For a single crystal, call ICrystal.StoreData(). For independently loaded nodes, call StoragePoint<T>.AddToSaveQueue() to schedule persistence or StoragePoint<T>.StoreData() to request it directly.

Check the CrystalResult returned by PrepareAndLoad and individual crystal saves. Failed preparation can be retried; IsPrepared alone does not confirm that every crystal loaded successfully. Store, StoreAndRelease, and StoreAndRip throw IOException for reported persistence failures. Unloaded and deleted crystals are skipped. These operations can also throw cancellation or serialization exceptions.

Snapshot and auxiliary-data saves await configured backup writes. A failed storage-point write retains its in-memory data and previous storage identifiers for retry. The clean-shutdown marker is written only after the data, journal, and supplement saves succeed.

Concurrency and durability

Persistence operations are serialized per crystal, and full-control saves are serialized per control. This does not make arbitrary application changes to ICrystal<T>.Data thread-safe. Coordinate root-data mutations with saves, or use the appropriate ValueLink isolation and locking model.

Use StoragePoint<T>.TryLock for mutations. Dispose its DataScope<T> before awaiting a save of that point or any containing crystal. StoreOnly and ForceRelease wait for the point's lock; TryRelease returns false if it cannot acquire the lock. Keep a consistent parent-to-child lock order. TryGet and PinData return references without granting exclusive mutation access; pinning only prevents data eviction.

Saving is not a transaction across crystals, snapshots, journal files, or backups. A failure can leave some writes completed. File-write completion does not guarantee survival of an immediate power loss. With histories disabled, snapshots are overwritten in place. Use histories and separate backups for recoverability, and test your application's recovery policy. Cross-process writers and different controls targeting the same files are not supported.

Journaling

Journaling records changes to Tinyhand structural objects between snapshots. Configure a journal and use [TinyhandObject(Structural = true)] on journaled data:

[TinyhandObject(Structural = true)]
public partial class JournalData
{
    [Key(0)]
    public partial int Count { get; set; }
}

var product = new CrystalUnit.Builder()
    .ConfigureCrystal(context =>
    {
        context.SetJournal(
            new SimpleJournalConfiguration(
                new LocalDirectoryConfiguration("Data/Journal")));

        context.AddCrystal<JournalData>(
            new CrystalConfiguration(
                new LocalFileConfiguration("Data/JournalData.tinyhand"))
            {
                NumberOfFileHistories = 3,
            });
    })
    .Build();

Structural members must be compatible with Tinyhand's structural serialization rules. ValueLink collections can also be used as journaled roots.

Auxiliary storage

StoragePoint<T> keeps a child object in an independently loaded file. Configure auxiliary storage on the owning crystal:

context.AddCrystal<RootData>(
    new CrystalConfiguration(new LocalFileConfiguration("Data/Root.tinyhand"))
    {
        StorageConfiguration = new SimpleStorageConfiguration(
            new LocalDirectoryConfiguration("Data/Storage"))
        {
            NumberOfHistoryFiles = 3,
        },
    });

The type containing a storage point must be a Tinyhand structural object. StoragePoint<T> reserves Tinyhand key 0; derived storage-point types must start their own keys at 1.

Read with TryGet. Use TryLock whenever data will be changed, and dispose the returned DataScope<T> to release the lock:

using (var scope = await root.Child.TryLock(AcquisitionMode.GetOrCreate))
{
    if (scope.IsValid)
    {
        scope.Data.Count++;
    }
}

// The mutation lock has been released; saving can now acquire it.
await root.Child.StoreData(StoreMode.StoreOnly);

Avoid replacing a storage-point instance with Set unless instance replacement is specifically required.

S3 storage

Supply bucket credentials through IStorageKey, then use an S3 file or directory configuration:

var storageKey = services.GetRequiredService<IStorageKey>();
storageKey.AddKey(
    "my-bucket",
    new AccessKeyPair("ACCESS_KEY_ID", "SECRET_ACCESS_KEY"));

var configuration = new CrystalConfiguration(
    new S3FileConfiguration("my-bucket", "app/FirstData.tinyhand"));

Do not embed production credentials in source code. Provide them through the application's secret-management mechanism.

Recovery

PrepareAndLoad(useQuery: true) consults the registered ICrystalDataQuery when recovery requires a decision. Pass false for non-interactive startup behavior. The second argument, loadCrystals, can defer loading after a clean shutdown; recovery after an unclean shutdown always loads crystals and replays the journal.

CrystalData checks the primary snapshot, available histories, and configured backups. For previously stored data with RequiredForLoading = true, a load failure is passed to ICrystalDataQuery; initialization fails when the query chooses to abort.

History snapshots and auxiliary data are hash-checked before deserialization. A damaged primary snapshot can fall back to a valid backup with the same waypoint. Recovery queries may choose to reconstruct defaults when data cannot be loaded; review that policy before using unattended startup with valuable data.

Samples

  • QuickStart contains the smallest complete application.
  • Advanced covers backups, dynamic configuration, journals, paths, dependency injection, save timing, and storage points.

Testing

dotnet test CrystalData.slnx -c Release

The persistence regression tests cover concurrent registration and saves, journal append/save contention, failed primary and backup writes, retry behavior, corrupt hashes, short reads, storage-point locks, pinned data, and clean-shutdown metadata. Tests use isolated local files; they do not replace S3 integration tests or process-crash and power-loss testing.

To collect coverage with Microsoft's dotnet-coverage tool:

dotnet tool install dotnet-coverage --tool-path artifacts/tools
artifacts/tools/dotnet-coverage collect "dotnet test CrystalData.slnx -c Release --no-build --no-restore" -f cobertura -o artifacts/coverage.cobertura.xml

Build the tests before using --no-build. Coverage includes source-generated code; inspect the CrystalData package and uncovered hand-written source separately.

See the persistence review for measured coverage, regression coverage, and verification limits.

License

CrystalData is licensed under the MIT License.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on CrystalData:

Package Downloads
Lp

Lp is an experimental program that create value.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.48.0 0 9/7/2026
0.47.0 55 9/4/2026
0.46.2 62 9/1/2026
0.46.1 100 8/21/2026
0.46.0 103 8/20/2026
0.45.4 175 5/14/2026
0.45.3 165 5/11/2026
0.45.2 136 5/10/2026
0.45.0 131 5/8/2026
0.44.0 160 4/23/2026
0.43.1 130 4/23/2026
0.43.0 124 4/22/2026
0.42.3 138 4/16/2026
0.42.2 139 4/7/2026
0.42.1 128 4/7/2026
0.42.0 132 4/6/2026
0.41.1 137 4/6/2026
0.41.0 140 4/5/2026
0.40.0 131 4/3/2026
0.39.3 140 3/31/2026
Loading failed