Pants is an embedded transactional database for .NET 10. It provides an idiomatic async API, snapshot reads, atomic writes, column families, range scans, time-to-live values, and local or cloud-backed persistence.
Pants is a good fit when an application needs to own its data without running a separate database service.
Add the core package:
dotnet add package Cntryl.Pants.CoreLibraries that only need to expose Pants contracts can instead reference
Cntryl.Pants.Abstractions without taking a dependency on the storage engine.
For Microsoft dependency injection, also add:
dotnet add package Cntryl.Pants.DependencyInjectionUpgrading from 1.x? See the Pants 2.0 migration guide.
Open a local database, write a value, and read it back:
using Cntryl.Pants;
using Cntryl.Pants.Transactions;
await using var database = await PantsDatabase.OpenAsync(
PantsOpenOptions.Local("data/catalog"));
var products = database.ColumnFamilies.DefaultFamily;
await using (var transaction = await database.Transactions.BeginAsync(
products,
PantsTransactionMode.ReadWrite))
{
transaction.Put("product:42"u8.ToArray(), "Coffee"u8.ToArray());
await transaction.CommitAsync(PantsWriteOptions.Sync);
}
await using (var transaction = await database.Transactions.BeginAsync(
products,
PantsTransactionMode.ReadOnly))
{
var value = await transaction.GetAsync("product:42"u8.ToArray());
Console.WriteLine(value is null
? "Not found"
: System.Text.Encoding.UTF8.GetString(value.Value.Span));
}PantsOpenOptions.Local reopens the same database from the supplied directory. Use
PantsOpenOptions.InMemory() for tests, caches, and short-lived data.
Transactions operate on one column family and provide a consistent snapshot. Dispose every transaction with
await using.
await using var transaction = await database.Transactions.BeginAsync(
database.ColumnFamilies.DefaultFamily,
PantsTransactionMode.ReadWrite,
cancellationToken);
transaction.Put(key, value);
transaction.Insert(uniqueKey, value);
transaction.Delete(obsoleteKey);
transaction.DeleteRange(startInclusive, endExclusive);
await transaction.CommitAsync(PantsWriteOptions.Sync, cancellationToken);Putadds or replaces a value.Insertrequires the key not to exist.Deleteremoves one key.DeleteRangeremoves keys in a half-open range.AssertValueadds a compare-and-set precondition to the transaction.RollbackAsyncends a transaction explicitly without committing it. Disposal also abandons an uncommitted transaction.
By default, concurrent writes use last-write-wins behavior. Request conflict detection when the application should retry instead:
transaction.SetConflictPolicy(PantsConflictPolicy.AbortOnWriteConflict);A conflict raises PantsWriteConflictException.
Choose durability when committing:
| Option | Use when |
|---|---|
PantsWriteOptions.Sync |
The commit must be durable on local storage before returning. |
PantsWriteOptions.Buffered |
Buffered local durability is sufficient. |
PantsWriteOptions.BestEffort |
The data may be lost if the process exits unexpectedly. |
PantsWriteOptions.CloudAsync |
Local acknowledgement may precede cloud persistence. |
PantsWriteOptions.CloudStrict |
Cloud persistence must complete before returning. |
Use Sync unless the application has made an explicit durability tradeoff. Cloud options require cloud-backed storage.
Use column families to keep independent groups of keys in the same database:
var sessions = await database.ColumnFamilies.CreateAsync("sessions", cancellationToken);
var existing = await database.ColumnFamilies.GetAsync("sessions", cancellationToken);
var all = await database.ColumnFamilies.ListAsync(cancellationToken);Pass the selected column family to Transactions.BeginAsync. Dropping a column family is destructive:
await database.ColumnFamilies.DropAsync(sessions, cancellationToken);Scans are ordered async streams over a transaction's snapshot:
using Cntryl.Pants.Scan;
await using var transaction = await database.Transactions.BeginAsync(
database.ColumnFamilies.DefaultFamily,
PantsTransactionMode.ReadOnly,
cancellationToken);
await using var scan = await transaction.ScanAsync(
new PantsScanQuery
{
Prefix = "product:"u8.ToArray(),
Direction = PantsScanDirection.Forward,
Limit = 100
},
cancellationToken);
await foreach (var entry in scan.WithCancellation(cancellationToken))
{
Console.WriteLine(System.Text.Encoding.UTF8.GetString(entry.Key.Span));
}Use StartInclusive and EndExclusive for a bounded range, Prefix for prefix matching,
Direction for forward or reverse iteration, and Limit to cap the number of results.
Pass a time-to-live to Put or Insert:
transaction.Put(
"session:abc"u8.ToArray(),
sessionBytes,
timeToLive: TimeSpan.FromHours(1));Expired values are no longer returned by reads or scans.
Start with the defaults. Select a performance goal and workload profile only when they describe a known workload:
using Cntryl.Pants.Storage;
var options = PantsOpenOptions.Local("data/catalog")
.WithPerformanceGoal(PantsPerformanceGoal.Latency)
.WithWorkloadProfile(PantsWorkloadProfile.ReadMostly)
.WithMemoryBudget(PantsMemoryBudget.FromBytes(512L * 1024 * 1024));Available performance goals are Latency, Throughput, and Economy. Workload profiles are
Mixed, WriteHeavy, ReadMostly, RangeScan, and TtlHeavy.
Options are immutable input. PantsOpenOptionsValidator.Validate or database open resolves them into a validated
runtime plan. Prefer the high-level goal, profile, and memory budget controls over low-level tuning. New code can
construct grouped options directly with PantsOpenOptions.Create.
Register a lazily opened database with the Microsoft dependency injection container:
using Cntryl.Pants;
using Microsoft.Extensions.DependencyInjection;
services.AddPants(PantsOpenOptions.Local("data/catalog"));Host-based applications can bind and validate settings through the standard options pattern:
services.AddPants().BindConfiguration("Pants");{
"Pants": {
"Storage": {
"Kind": "Local",
"Path": "data/catalog"
},
"PerformanceGoal": "Latency",
"WorkloadProfile": "ReadMostly"
}
}Bound settings are validated when the host starts and projected once into immutable
PantsOpenOptions when the database first opens. Use AddKeyedPants("name") for named options and multiple independent
databases. See dependency injection configuration
for the complete storage, cloud-provider, and credential shapes.
Resolve IPantsDatabaseProvider and open the shared database when it is first needed:
public sealed class CatalogStore(IPantsDatabaseProvider databaseProvider)
{
public ValueTask<IPantsDatabase> GetDatabaseAsync(
CancellationToken cancellationToken = default) =>
databaseProvider.GetDatabaseAsync(cancellationToken);
}The service provider owns the database and closes it during asynchronous disposal.
Pants supports Amazon S3, S3-compatible services, Azure Blob Storage, Google Cloud Storage, and first-class Oracle Cloud Infrastructure Object Storage configuration through OCI's S3 Compatibility API. Cloud storage uses a local cache directory and a provider location:
using Cntryl.Pants.Cloud;
var location = new PantsCloudStorageLocation(
new PantsAwsS3Provider(
Bucket: "catalog-production",
Region: "us-east-1",
Credentials: new PantsS3CredentialSource.AwsDefaultChain()),
Prefix: "pants/catalog");
var options = PantsOpenOptions.Cloud(
localCachePath: "data/catalog-cache",
location: location);
await using var database = await PantsDatabase.OpenAsync(options, cancellationToken);Prefer environment, workload-identity, managed-identity, or default credential chains over static credentials. Use
PantsOpenOptions.CloudMulti only when WAL, SST, and control data need separate locations. Custom backends can
implement IPantsCloudProvider and the object-store primitive
IPantsCloudObjectStore; Pants continues to own its object layout, WAL/SST formats, leases, and fencing. Validate
configuration without I/O and optionally run a read-only provider preflight before opening;
see cloud validation and preflight.
Pants exposes snapshots suitable for health endpoints and metrics collection:
var runtime = await database.Diagnostics.GetRuntimeMetricsAsync(cancellationToken);
var recovery = await database.Diagnostics.GetRecoveryMetricsAsync(cancellationToken);
var readAmplification = await database.Diagnostics.GetReadAmplificationMetricsAsync(cancellationToken);Verify an open database:
var report = await database.PersistentStorage!.VerifyAsync(
timeout: TimeSpan.FromSeconds(30),
cancellationToken);Or verify a closed local database path:
var report = await PantsDatabase.VerifyPathAsync("data/catalog", cancellationToken);Prefer await using, which closes the database during disposal. For applications with a bounded shutdown phase, call
ShutdownAsync explicitly:
await database.ShutdownAsync(TimeSpan.FromSeconds(30), cancellationToken);Stop accepting new work before shutdown and allow outstanding transactions to finish or be cancelled.
Public operations accept CancellationToken. Pants reports database failures through
PantsException subclasses, including:
PantsWriteConflictExceptionfor a rejected concurrent writePantsBusyExceptionandPantsWriteStallExceptionfor temporary pressurePantsFencedExceptionand lease exceptions when a writer no longer owns the databasePantsCorruptionExceptionandPantsRecoveryFailedExceptionfor storage or recovery failures
Handle only errors the application can meaningfully recover from; let unexpected database failures reach normal application diagnostics.
- .NET 10 or later
- one application writer per local database path
- asynchronous disposal of databases, transactions, and scans
Run the standard repository checks with dotnet build and dotnet test. Cloud provider behavior is qualified against
the pinned Sqrzl emulator; see
Cloud provider qualification for the Compose and test commands.