Developer guide

Technical documentation

Use the NetlyDB SDK to embed a document database in any .NET application, register typed collections through dependency injection, store binary files in dedicated file (blob) collections, and query documents with familiar LINQ expressions or plain-text filter syntax.

Getting started

Add the NetlyDB.SDK package to your project, register NetlyDB during application startup, and inject INetlyDBCollection<T> wherever you need typed document access.

Minimal setup

// Program.cs or startup
builder.Services
    .AddNetlyDB()
    .AddNetlyDBCollection<Order>();

var app = builder.Build();

var orders = app.Services.GetRequiredService<INetlyDBCollection<Order>>();

The default collection name is derived from the document type (for example User maps to a User collection). Call AddNetlyDBCollection<T>() once per document type you want in dependency injection.

Using a collection

var id = orders.Add(new Order
{
    Reference = "SO-10042",
    CustomerName = "Contoso",
    Country = "BE",
    Status = "open",
    Total = 1299.50m
});

var order = orders.Find(id);

var openInBelgium = await orders.ListAsync(x => x.Country == "BE" && x.Status == "open");

orders.Update(id, order with { Status = "shipped" });
orders.Delete(id);

Configuration

Pass a delegate to AddNetlyDB to configure NetlyDBOptions. All options are optional; sensible defaults apply for embedded single-node use.

services.AddNetlyDB(options =>
{
    options.EnableConsoleLogs = false;
    options.EnableGrpcHosting = true;
    options.GrpcPort = 9025;
    options.ShardCount = 4;
    options.QueryLogRetention = TimeSpan.FromDays(7);
})
.AddNetlyDBCollection<Order>();
OptionDescriptionDefault
EnableConsoleLogs Writes structured log events to the console. true
EnableGrpcHosting Hosts the gRPC endpoint used by cluster replication and remote clients. true
GrpcPort Port for the gRPC service. 9025
BrokerHost / BrokerPort Event broker used for logging and operational events. localhost / 9023
ShardCount Number of shards for document distribution. 4
EventLogRetention How long event log entries are kept. 30 days
QueryLogRetention How long query history entries are kept. 3 days
EnableTtlCleanup Background cleanup of documents with a TTL. true
SelfAddress / Port Required when running as a cluster node. null
NodeFocus Performance tuning mode for the node. Speed
License Enterprise license (key, file path, or env var). See Licensing. Community

Optional modules

NetlyDB is modular. Enable only what your deployment needs. Several modules are Enterprise features that require a license — see Licensing.

  • AddNetlyDBAPI(port) — self-hosted REST API (default port 9020).
  • AddNetlyDBManagementUI(webPort, apiPort) — operational Management UI (default web port 9021).
  • AddNetlyDBMCP(port, path, maxResultLimit, allowedCollections) — Model Context Protocol server for AI tooling (default port 9026).
  • AddRemoteNetlyDB(remoteAddress, port) — gRPC client; replaces the local store with a remote node.
  • JoinCluster(seedNodes) — distributed mode; contact seed nodes to join a cluster.
  • UseStorageProvider<T>(rootPath) — swap file-system storage (for example Azure Blob).
services.AddNetlyDB(options => options.EnableConsoleLogs = false)
    .AddNetlyDBCollection<User>()
    .AddNetlyDBAPI(port: 9020)
    .AddNetlyDBManagementUI(webappPort: 9021, apiPort: 9020)
    .AddNetlyDBMCP(port: 9026, path: "/mcp", maxResultLimit: 100)
    .JoinCluster("10.0.0.12", "10.0.0.13");

Named collections

Register multiple logical collections for the same document type:

services.AddNetlyDB()
    .AddNetlyDBCollection<AuditEntry>("audit")
    .AddNetlyDBCollection<AuditEntry>("security");

var provider = app.Services.GetRequiredService<INamedNetlyDBCollectionProvider<AuditEntry>>();
var audit = provider.Get("audit");
var security = provider.Get("security");

Licensing

NetlyDB is free to use as an embedded, single-node database — the Community edition. Enterprise features require a valid license: clustering & replication, the REST API, the Management UI, the MCP server, encryption at rest, JWT/OIDC authentication, and observability export. Licenses are signed and verified entirely offline — there is no license server or phone-home.

Applying a license

Provide the license token in any one of these ways (they are checked in this order):

  • In code — set options.License.LicenseKey to the license token string.
  • File path — set options.License.LicenseFilePath to a file that contains the token.
  • Environment variable — set NETLYDB_LICENSE (name configurable via options.License.EnvironmentVariable).
// 1) Inline license key (e.g. from configuration or a secret store)
services.AddNetlyDB(options =>
{
    options.License.LicenseKey = "NLDB1.eyJ2Ijox...";
});

// 2) From a license file
services.AddNetlyDB(options =>
{
    options.License.LicenseFilePath = "/etc/netlydb/netlydb.lic";
});

// 3) From an environment variable (default: NETLYDB_LICENSE) — no code required
//    setx NETLYDB_LICENSE "NLDB1.eyJ2Ijox..."
services.AddNetlyDB();

Enforcement & grace period

With no license the process runs as Community. If you enable an Enterprise feature without a valid license, startup fails closed with a clear error instead of silently running degraded. An expired license keeps working during a configurable grace period (options.License.GracePeriod, default 14 days) so renewals never cause a hard outage. You can review the active license and per-feature entitlements anytime in the Management UI under Settings → License.

Enterprise features

FeatureEnabled by
Clustering & replicationJoinCluster(...)
REST APIAddNetlyDBAPI(...)
Management UIAddNetlyDBManagementUI(...)
MCP serverAddNetlyDBMCP(...)
Encryption at restSecurity.Encryption.Enabled
Advanced auth (JWT/OIDC)Security.EnableJwtBearer
Observability exportmetrics & health endpoints

File and blob collections

Besides typed and dynamic document collections, NetlyDB provides file collections for arbitrary binary data. Each file is a FileData payload (name, extension, content type, bytes, optional virtual folder path, and tags). Blobs are persisted compressed on the configured storage provider; collection metadata and tags remain available without loading full file content.

Resolve IFileStore from dependency injection (registered automatically by AddNetlyDB). Call GetCollection(name) to open or create a collection, or CreateCollection(name) to register one explicitly (also used by the REST API and Management UI).

using NetlyDB.Core.Collections.FileCollections;
using NetlyDB.Core.Stores;

// IFileStore is registered by AddNetlyDB()
var fileStore = app.Services.GetRequiredService<IFileStore>();

fileStore.CreateCollection("uploads"); // optional; GetCollection also creates on first use
var uploads = fileStore.GetCollection("uploads");

Upload and read files

var bytes = await File.ReadAllBytesAsync("report.pdf");

var id = uploads.Add(new FileData
{
    FileName = "report.pdf",
    Extension = ".pdf",
    ContentType = "application/pdf",
    Bytes = bytes,
    VirtualFolderPath = "invoices/2026", // folder path shown in Management UI
    Tags = ["finance", "q1"]
});

// Stream decompressed content (preferred for large files)
await using var stream = uploads.OpenRead(id!.Value);

// Or load the full document (bytes populated on read)
var document = uploads.GetById(id!.Value) as FileDocument;

uploads.Delete(id!.Value);

Metadata, tags, and streaming

Use GetMetaData / GetAllMetaData to list files by name, size, content type, virtual folder, or tags without reading blob bytes. OpenRead returns a decompressed Stream for downloads and media playback; GetById loads the full FileDocument including bytes when needed.

FileMetaData meta = uploads.GetMetaData(id!.Value);
// meta.FileName, meta.Size, meta.ContentType, meta.VirtualFolderPath, meta.Tags

foreach (var entry in uploads.GetAllMetaData())
{
    Console.WriteLine($"{entry.FileName} ({entry.Size} bytes)");
}

Tag queries

Filter files by one or more tags with WhereByTagAsync or WhereByTagsAsync (match all tags by default).

var gallery = await uploads.WhereByTagAsync("gallery");
var q1Reports = await uploads.WhereByTagsAsync(
    ["finance", "q1"],
    matchAll: true);

Management UI

Enable AddNetlyDBManagementUI to manage file collections in the browser alongside documents and cluster operations. The file browser renders a folder tree from each file’s VirtualFolderPath so you can structure uploads into nested folders (set the folder when uploading or via FileData.VirtualFolderPath in code).

  • Thumbnails — images and videos show a thumbnail in the file list.
  • Video preview — hover a video thumbnail to play a short inline preview before opening the full file.
  • Tag search — filter the list by one or more tags (match all or any).
  • Upload — add files to the current collection and target folder from the UI.

REST API

When AddNetlyDBAPI is enabled, create a file collection with:

POST /Collections/file/uploads
GET  /Collections/uploads/count
GET  /Collections/names

File collections appear alongside document collections in GET /Collections/names and GET /Collections/infos.

Storage backends

By default, blobs are stored on the local file system under the NetlyDB data folder. Use UseStorageProvider<T>(rootPath) on the NetlyDB builder (for example AzureBlobStorageProvider) to persist file shards and snapshots to cloud blob storage in distributed deployments.

Collections API

INetlyDBCollection<T> is the application-facing wrapper around a typed document collection.

MemberPurpose
Add / AddRange Insert one or many documents; optional per-document TTL.
Find / FindAsync Load a document by id.
All / AllAsync Return every document in the collection.
Query() Build a fluent LINQ or string filter query.
ListAsync / FirstOrDefaultAsync / AnyAsync / CountAsync Query with LINQ or plain-text filter.
Update Replace a document by id.
UpdateWhereAsync Patch fields on all documents matching a LINQ predicate.
Delete / DeleteWhereAsync Remove by id or by LINQ predicate.
CreateIndex Declare an index from a property expression.

For projections, aggregates, and text-based update / delete statements, use the lower-level IDocumentCollection<T> (also registered in DI) via QueryAsync, SelectAsync, and related methods.

LINQ queries

The fluent Query() API composes filters with Where, sorts with OrderBy / OrderByDescending, and pages with Skip / Take. Multiple Where calls are combined with logical and.

var page = await orders.Query()
    .Where(x => x.Country == "BE")
    .Where(x => x.Total >= 100)
    .OrderByDescending(x => x.Total)
    .Skip(0)
    .Take(25)
    .ToListAsync();

Shortcut methods

These accept either a LINQ predicate or a plain-text filter string:

// LINQ predicate
var shipped = await orders.ListAsync(x => x.Status == "shipped");

// Plain-text filter (same syntax as Management UI)
var shippedText = await orders.ListAsync("Status == \"shipped\"");

var count = await orders.CountAsync("Country == \"BE\"");
var exists = await orders.AnyAsync(x => x.Total > 10_000);

Supported LINQ patterns

  • Equality and inequality: ==, !=
  • Comparisons: <, >, <=, >=
  • Logical combinations: &&, || (via chained Where or a single expression)
  • String helpers: StartsWith, EndsWith, Contains, and negation with !
  • Nested properties: x.Address.Country == "BE"
await orders.ListAsync(x => x.CustomerName.StartsWith("Cont"));
await orders.ListAsync(x => x.Status != "cancelled");
await orders.ListAsync(x => !x.Reference.EndsWith("-TEMP"));
await orders.ListAsync(x => x.Country == "BE" && x.LineCount >= 3);

Bulk updates and deletes

var updated = await orders.UpdateWhereAsync(
    x => x.Status == "open" && x.Country == "BE",
    u => u.Set(x => x.Status, "processing"));

var removed = await orders.DeleteWhereAsync(x => x.Status == "cancelled");

Indexes

Create indexes with expression selectors to speed up filters on those fields (including nested and compound keys):

orders.CreateIndex(x => x.Country);
orders.CreateIndex(x => x.Status);
orders.CreateIndex(x => new { x.Country, x.Status });

Plain-text query syntax

Pass a filter string to ListAsync, FirstOrDefaultAsync, CountAsync, AnyAsync, or Query().Where(string). Syntax matches the Management UI query language.

Filter operators

OperatorExample
== Country == "BE"
!= Status != "cancelled"
>, >=, <, <= Total >= 100 && LineCount < 5
&& Country == "BE" && Status == "shipped"
like CustomerName like "Acme%"
not like Reference not like "TMP%"
in Country in ("BE", "NL", "DE")
skip / limit Status == "open" skip 20 limit 10
order by Country == "BE" order by Total desc limit 25

Examples

Single property

Match one field to a literal value.

Country == "BE"
await orders.ListAsync("Country == \"BE\"");

Multiple conditions

Combine predicates with &&.

Country == "BE" && Status == "shipped" && Total >= 500
await orders.Query()
    .Where("Country == \"BE\" && Status == \"shipped\" && Total >= 500")
    .ToListAsync();

Wildcard search

Use like with % as in SQL.

CustomerName like "Net%"
await orders.FirstOrDefaultAsync("CustomerName like \"Net%\"");

Paging

Skip and limit can appear in any order after the filter.

Status == "open" skip 10 limit 25
await orders.ListAsync("Status == \"open\" skip 10 limit 25");

Sorting

Order results before paging when using string queries on the collection.

Country == "BE" order by Total desc limit 10
// Prefer Query().OrderBy(...) for LINQ; order by is built into text queries on IDocumentCollection.

SELECT queries

Use IDocumentCollection<T>.SelectAsync or QueryAsync when you need field projection, aggregates, grouping, or raw rows without document metadata wrappers.

var collection = app.Services.GetRequiredService<IDocumentCollection<Order>>();

// Project fields
var rows = await collection.SelectAsync(
    "select Reference, CustomerName, Total where Country == \"BE\" limit 50");

// Raw rows (no document metadata wrapper)
var raw = await collection.SelectAsync(
    "select raw Reference, Total where Status == \"open\"");

// Aggregates
var stats = await collection.SelectAsync(
    "select Country, sum(Total) as Revenue, count(*) as OrderCount group by Country");

Clauses can be combined: where, order by, group by, skip, and limit.

UPDATE and DELETE queries

Text mutations run through QueryAsync on IDocumentCollection<T>:

var collection = app.Services.GetRequiredService<IDocumentCollection<Order>>();

await collection.QueryAsync(
    "update set Status = \"archived\" where Country == \"BE\" && CreatedAt < DateTime(\"2024-01-01T00:00:00Z\")");

await collection.QueryAsync("delete where Status == \"cancelled\"");

delete * truncates the entire collection. All other deletes require an explicit where clause.

Document metadata

Filter on system fields with the Meta. prefix:

await orders.ListAsync("Meta.CreatedAt >= DateTime(\"2025-01-01T00:00:00Z\")");

// Meta.Id uses the document identifier string
await orders.FirstOrDefaultAsync("Meta.Id == \"b549ea4b-0040-4d16-9dfe-0c6c06ca16fa\"");

Solution packages

  • NetlyDB.SDK — dependency injection extensions, INetlyDBCollection<T>, cluster and remote client wiring.
  • NetlyDB.Core — document engine, file collections, query pipeline, indexing, persistence, replication.
  • NetlyDB.API — optional REST API host (document and file collection endpoints).
  • NetlyDB.MCP — optional MCP server for AI assistants.
  • NetlyDB.Licensing — offline license verification and Enterprise-feature gating (bundled with the SDK).
An unhandled error has occurred. Reload X