Developer guide

Technical documentation

Use the NetlyDB SDK to embed a document database in any .NET application, register typed collections through dependency injection, and query 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

Optional modules

NetlyDB is modular. Enable only what your deployment needs:

  • 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");

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, query pipeline, indexing, persistence, replication.
  • NetlyDB.API — optional REST API host.
  • NetlyDB.MCP — optional MCP server for AI assistants.
An unhandled error has occurred. Reload X