TriCoreDb 0.3.0
dotnet add package TriCoreDb --version 0.3.0
NuGet\Install-Package TriCoreDb -Version 0.3.0
<PackageReference Include="TriCoreDb" Version="0.3.0" />
<PackageVersion Include="TriCoreDb" Version="0.3.0" />
<PackageReference Include="TriCoreDb" />
paket add TriCoreDb --version 0.3.0
#r "nuget: TriCoreDb, 0.3.0"
#:package TriCoreDb@0.3.0
#addin nuget:?package=TriCoreDb&version=0.3.0
#tool nuget:?package=TriCoreDb&version=0.3.0
TriCoreDb for .NET
Official .NET client for TriCoreDB, speaking
the native tricore wire protocol. It covers SQL (with server-side parameters and session
transactions), documents, vectors, graphs, cache and LLM context export over one
connection.
- No dependencies beyond the .NET base class library.
- Async throughout, with
CancellationTokenon every call. - Server-side parameters. Values never become part of the SQL text.
- Transactions, connection pooling, TLS and mutual TLS.
Requirements
- .NET 8.0 or later
- A TriCoreDB server speaking protocol 1.0 (
tricore-server0.1.0-rc.1 or later). See Running a server.
Installation
dotnet add package TriCoreDb
using TriCoreDb;
Running a server
The quickest way is the official Docker image,
trinesh14/tricoredb.
Local development (no TLS and no encryption, for this machine only). Set
TRICORE_ADMIN_PASSWORD in your shell first. Then create the admin and start the server:
docker run --rm -v tricoredb-dev:/var/lib/tricoredb -e TRICORE_ADMIN_PASSWORD --entrypoint /usr/local/bin/tricore trinesh14/tricoredb:0.1.0-rc.1-r2 auth init-admin --user admin --password-env TRICORE_ADMIN_PASSWORD --data-dir /var/lib/tricoredb/data
docker run -d --name tricoredb-dev -p 127.0.0.1:8427:8427 -e TRICORE_TLS=off -e TRICORE_ENCRYPTION=off -e TRICORE_MODULES=all -v tricoredb-dev:/var/lib/tricoredb trinesh14/tricoredb:0.1.0-rc.1-r2
Anything else: by default the image runs with TLS on and an encrypted data volume. Follow the quick start on the Docker Hub page to create the certificate and key, then connect with TLS as shown below.
TRICORE_MODULES=all enables every data model. The image's default is sql, document
and cache; a call to a disabled model throws a TriCoreException whose ErrorCode is
engine.disabled.
Connect
await using var db = await TriCoreClient.ConnectAsync("127.0.0.1", 8427, "admin", "pw");
8427 is the server's default port. TLS stays off until you pass a TlsOptions. On a
plaintext connection the secret crosses the wire in the clear.
var tls = new TlsOptions
{
CaFile = "/etc/tricore/ca.pem", // the PEM bundle that signed the server certificate
ServerName = "db.internal", // SNI, and the name checked in the certificate
};
await using var db = await TriCoreClient.ConnectAsync("db.internal", 8427, "admin", "pw", tls: tls);
When TLS is on, the driver verifies the chain against CaFile alone and checks the
hostname. It does not fall back to the OS root store, so leaving out CaFile gives an
empty trust store and the connection fails closed. DangerAcceptInvalidCerts = true turns
verification off and is meant for development only. For mTLS, set both ClientCertFile
and ClientKeyFile. A Pool applies the same TlsOptions to every connection it opens.
| Property | Default | Meaning |
|---|---|---|
CaFile |
null (empty trust store) |
PEM bundle used to verify the server |
ServerName |
"localhost" |
Expected server name (SNI and certificate check) |
ClientCertFile |
null |
PEM client certificate chain (mTLS) |
ClientKeyFile |
null |
PEM private key for that certificate (mTLS) |
DangerAcceptInvalidCerts |
false |
Development only. Skips all verification |
ConnectAsync also accepts connectTimeout, which bounds TCP, TLS, HELLO and AUTH, and
readTimeout, which bounds each reply. They are separate deadlines, so a short connect
budget never turns into a short query deadline.
Quick start
using System.Text;
using TriCoreDb;
await using var db = await TriCoreClient.ConnectAsync("127.0.0.1", 8427, "admin", "pw");
await db.CacheSetAsync("app", "greeting", Encoding.UTF8.GetBytes("world"));
string? hello = await db.CacheGetTextAsync("app", "greeting");
await db.ExecuteAsync("CREATE TABLE t (id INT PRIMARY KEY, name TEXT)");
await db.ExecuteAsync("INSERT INTO t VALUES (?, ?)", new object?[] { 1, "ada" });
Rows rows = await db.QueryAsync("SELECT id, name FROM t WHERE id = ?", new object?[] { 1 });
Console.WriteLine($"{rows.Columns[1]} = {rows[0][1]}");
SQL
QueryAsync runs SELECT only. It is a read-only boundary, so a caller that holds only
read permission can never write through it. ExecuteAsync runs writes and DDL.
Parameters are bound on the server
Each ? placeholder is bound server-side. The values travel beside the statement as a
typed JSON array, so a value can never turn into syntax. This needs the SERVER_PARAMS
feature bit, which is negotiated at HELLO (db.ServerParamsGranted). If the server did not
grant it, the overloads that take arguments throw by name and send nothing. They never
fall back to pasting values into the SQL text. If you really want client-side rendering,
call SqlParams.Bind(sql, args) yourself.
How .NET values go on the wire:
| .NET value | Wire form | Notes |
|---|---|---|
int, long, other integers |
JSON number | |
ulong above long.MaxValue |
exact JSON number | Never narrowed. The server binds it as a decimal and the column decides whether it fits. |
double, float |
JSON number | NaN and infinities are refused |
decimal |
text, ToString(CultureInfo.InvariantCulture) |
Every digit survives |
byte[], ReadOnlyMemory<byte> |
"0x" + lowercase hex |
For BLOB columns |
bool, null, DBNull |
JSON true/false/null |
|
string, char, Guid, DateTime |
JSON string |
The decimal trade-off. A decimal goes out as text because a JSON number is read
through a double, which would lose the digits a decimal exists to keep. The server
accepts text for a DECIMAL column but not for a DOUBLE one, so a decimal bound into a
DOUBLE column fails by name. Pass a double for a DOUBLE column.
Transactions
The two shapes below behave differently.
TransactionAsync(params SqlStatement[])sends a wholeBEGIN … COMMITscript as one request. It works on every node, including sharded and forwarding nodes.BeginAsync/CommitAsync/RollbackAsynckeep a rollback boundary open across requests on this connection. They need theSESSION_TXNfeature bit (db.SessionTxnGranted). Without itBeginAsyncthrows by name before sending anything, and never falls back to silent autocommit.
await db.WithTransactionAsync(async tx => // commits on return
{
await tx.ExecuteAsync("UPDATE accounts SET balance = balance - 10 WHERE id = ?", new object?[] { 1 });
await tx.ExecuteAsync("UPDATE accounts SET balance = balance + 10 WHERE id = ?", new object?[] { 2 });
}); // rolls back and rethrows on any exception
await using (var tx = await db.BeginTransactionAsync()) // disposing rolls back unless committed
{
await db.ExecuteAsync("INSERT INTO t VALUES (5, 'eve')");
await tx.CommitAsync();
}
A block belongs to its connection's socket. The server refuses a COMMIT sent from any
other connection, and a dropped socket rolls the block back. Pool.UseAsync never returns
a connection to the pool with a block still open: it rolls the block back first and then
throws at the callback that left it open.
ConnectAsync(features:) overrides the feature bitmap sent in HELLO. For example,
TriCoreClient.Features & ~TriCoreClient.FeatureSessionTxn masks one bit out.
Pool
await using var pool = new Pool("127.0.0.1", 8427, "admin", "pw", size: 8);
long n = await pool.UseAsync(async c => long.Parse((await c.QueryAsync("SELECT COUNT(*) FROM t"))[0][0]));
The pool fills lazily and stays bounded. When it is exhausted, a caller waits until its
timeout and then gets PoolTimeoutException; the pool never grows. A server-side error
leaves the connection in the pool, while a poisoned or timed-out connection is retired.
Other families
Every method takes an optional trailing database (default "main") and a
CancellationToken.
- Cache: keys, TTLs and counters (
CacheSetAsync,CacheGetAsync,CacheIncrAsync,CacheSetNxAsync,CacheKeysAsync, …), lists (CacheLPushAsync…CacheLIndexAsync), sets (CacheSAddAsync…CacheSMembersAsync), hashes (CacheHSetAsync…CacheHLenAsync) and streams (CacheXAddAsync…CacheXTrimAsync). Values arebyte[], and the*Textoverloads handle UTF-8. A miss returnsnull, which is distinct from an empty value. - Cache, server 0.1.0-rc.3 and later: sorted sets (
CacheZAddAsync,CacheZRangeAsync,CacheZRangeByScoreAsync, …), locks with fencing tokens (CacheLockAcquireAsync,CacheLockRenewAsync,CacheLockReleaseAsync), bitmaps (CacheSetBitAsync…CacheBitOpAsync), HyperLogLog (CachePfAddAsync,CachePfCountAsync,CachePfMergeAsync),CacheMGetAsync/CacheMSetAsync,CacheSetIfAsync,CacheCompareAndSwapAsync,CacheGetDelAsync,CacheGetSetAsync,CacheGetExAsync, stale-while-revalidate reads (CacheSetFreshAsync,CacheGetFreshAsync), blocking pops (CacheBLPopAsync,CacheBRPopAsync), set algebra (CacheSUnionAsync,CacheSInterAsync,CacheSDiffAsync), pipelines, transactions and scripts built fromCacheCommand, and Pub/Sub by polling (CachePublishAsync,CachePollAsync). - Documents: collections,
DocumentInsertAsync,DocumentFindAsyncwithDocumentFilter,DocumentUpdateManyAsyncwithDocumentUpdate, secondary indexes, andDocumentAggregateAsyncwithAggregateStage. - Documents, server 0.1.0-rc.3 and later:
DocumentReplaceAsync,DocumentInsertManyAsync,DocumentDeleteManyAsync,DocumentBulkWriteAsyncwithBulkOp,DocumentExplainAsync, collection options (a schema, defaults and versions, asCollectionOptions), and transactions spanning requests. - Vectors:
VectorCreateCollectionAsync(cosine, dot or l2), upsert, get, delete,VectorSearchAsyncwith metadata filters, describe and list. - Vectors, server 0.1.0-rc.3 and later:
VectorUpsertManyAsync,VectorSearchBatchAsync,VectorSearchMultiAsync,VectorSearchByExamplesAsync, filters (VectorCountAsync,VectorDeleteByFilterAsync,VectorUpdateByFilterAsync), named vectors, aliases, the built-in embedder (VectorEmbedAsync,VectorUpsertTextAsync) and index maintenance. - Graph: nodes, edges, neighbours, traversal,
GraphShortestPathAsync,GraphWeightedShortestPathAsync, degree, paging, andGraphQueryAsyncfor the read-only Cypher subset. - Graph, server 0.1.0-rc.3 and later:
GraphAddManyAsync/GraphRemoveManyAsync, in-place updates,GraphDfsAsync,GraphSubgraphAsync,GraphKShortestPathsAsync,GraphAllPathsAsync, algorithms (components, cycles, centrality, communities, triangles, similarity, PageRank), indexes and constraints withGraphFindNodesAsync, Cypher writes throughGraphExecuteAsync, and graph transactions. - LLM:
LlmContextAsyncandLlmSchemaAsyncrender read-only context as TOON, JSON, Markdown or native output. - Admin:
AdminPingAsyncandAdminStatusAsync(these need[modules] cluster = true).PingAsyncis a transport-level liveness check.
RequestAsync(JsonObject op) sends any raw operation.
The methods marked "server 0.1.0-rc.3 and later" are answered with an error, raised as
TriCoreException, by an older server.
Document and graph transactions
A transaction is named by an id, and a request runs inside it when it carries that id.
RequestTxn puts the id on every request the client sends until the transaction ends:
var txn = await db.DocumentBeginTransactionAsync();
db.RequestTxn = txn;
await db.DocumentInsertAsync("orders", order);
await db.DocumentUpdateOneAsync("stock", "sku-1", DocumentUpdate.Inc("held", 1));
await db.DocumentCommitTransactionAsync(txn); // clears RequestTxn
Errors
Every failure the driver raises derives from TriCoreException. The subtypes are
AuthException, ProtocolException, PoolTimeoutException, TriCoreTimeoutException
(fatal to its connection) and RequestNotSentException.
ErrorCode holds the server's diagnostics.error_code. Branch on that code, not on the
message text.
Leader redirects
In a cluster only the leader accepts writes, and a follower refuses with not_leader. The
driver handles that itself (FollowLeader, on by default):
- when the refusal names the leader, it opens a connection there with the same user,
secret and TLS settings (the leader's host is the certificate name), sends the request
again, and keeps that connection (
Addressreports it); - while an election is in progress (no leader named), it waits and asks the same node
again, pausing 250 ms, then 500 ms, up to 2 s (
LeaderRetryDelay); - after
LeaderRetries(5) it throws the last refusal.
This is safe because a not_leader refusal was applied nowhere. Nothing is sent again
inside a session transaction, and never a refusal with
ErrorCode == TriCoreException.ErrorCodeOutcomeUnknown: a follower handed that request to
the leader and lost the answer, or the leader could not confirm it, so it may have been
applied. The leader's address must be reachable from your application; when it is not, the
driver keeps its connection and waits. A Pool has the same three properties and applies
them to every connection it opens.
To handle redirects yourself, set FollowLeader = false:
db.FollowLeader = false;
try { await db.ExecuteAsync("INSERT INTO t VALUES (1)"); }
catch (TriCoreException ex) when (ex.IsNotLeader)
{
// ex.LeaderHint is "host:port", or null when the leader is unknown (mid-election)
}
Response exposes the same ErrorCode, LeaderHint and IsNotLeader.
Hardening
- Inbound frames are capped at 64 KiB for control frames and a protocol ceiling for responses. The cap is checked before any buffer is allocated.
- A frame with an unknown version is rejected.
- Overlapping requests on one connection are refused with
RequestNotSentException, so one request can never read another's reply. - A connection whose stream can no longer be trusted is marked poisoned and refuses further use.
Building from source
scripts/build.ps1 # or scripts/build.sh
scripts/test.ps1 # xUnit; live tests start a private tricore-server
scripts/test.ps1 -Offline # only the tests that need no server
scripts/conformance.ps1 # cross-SDK conformance matrix (needs Node and the spec repo)
scripts/pack.ps1 # nupkg + snupkg into ./artifacts
Live tests look for the server binary in TRICORE_SERVER_BIN, then in a sibling
tricore/tricore-db/target/{release,debug} checkout. Each run starts its own server on an
ephemeral port with a temporary data directory and stops that process when it finishes.
With no binary to start, those tests are skipped rather than failed, so dotnet test
is green on a machine that has no server.
examples/TriCoreDb.Examples is a console app with one subcommand per family (basic,
sql, nosql, vector, graph, cache, errors, concurrency, all). It reads
TRICOREDB_HOST, TRICOREDB_PORT, TRICOREDB_USER and TRICOREDB_PASSWORD.
License
Apache-2.0. See LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. 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. |
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
See CHANGELOG.md.