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>();
| Option | Description | Default |
|---|---|---|
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.LicenseKeyto the license token string. - File path — set
options.License.LicenseFilePathto a file that contains the token. - Environment variable — set
NETLYDB_LICENSE(name configurable viaoptions.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
| Feature | Enabled by |
|---|---|
| Clustering & replication | JoinCluster(...) |
| REST API | AddNetlyDBAPI(...) |
| Management UI | AddNetlyDBManagementUI(...) |
| MCP server | AddNetlyDBMCP(...) |
| Encryption at rest | Security.Encryption.Enabled |
| Advanced auth (JWT/OIDC) | Security.EnableJwtBearer |
| Observability export | metrics & 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.
| Member | Purpose |
|---|---|
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 chainedWhereor 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
| Operator | Example |
|---|---|
| == | 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).