Repository navigation
Storing JSON
LottaDB lets you store JSON documents without any configuration. Just save it -- LottaDB handles the rest. All top-level properties are automatically queryable and searchable, just like POCOs.
No schema definition needed. All top-level simple-type properties are automatically:
- Stored as full-fidelity JSON in table storage
- Indexed in Lucene for search
-
Queryable via
JsonExpressionpredicates
var doc = JsonDocument.Parse("""{ "Name": "Alice", "Age": 30, "City": "Seattle" }""");
await db.SaveAsync(doc);
var key = doc.GetKey(); // auto-generated ULID
var etag = doc.GetETag(); // for optimistic concurrencyvar doc = await db.GetAsync(key);
doc.RootElement.GetProperty("Name").GetString(); // "Alice"await db.DeleteAsync(key);Use JsonExpression predicates to query by property:
// By property value
var results = db.Search(je => je["Name"] == "Alice").ToList();
// Range query
var results = db.Search(je => je["Age"] > 20 && je["Age"] <= 30).ToList();
// By schema type (when using discriminators)
var people = db.Search(je => je.GetSchema() == "Person").ToList();// All JSON documents
await foreach (var doc in db.GetManyAsync(je => true)) { ... }
// With filter
await foreach (var doc in db.GetManyAsync(je => je["Age"] > 20)) { ... }// By predicate
await db.DeleteManyAsync(je => je["Age"] > 50);
// By schema type
await db.DeleteManyAsync(je => je.GetSchema() == "Person");var results = await db.SaveManyAsync(new object[]
{
JsonDocument.Parse("""{ "Name": "Alice", "Age": 30 }"""),
JsonDocument.Parse("""{ "Name": "Bob", "Age": 25 }"""),
});Writes are batched transactionally (auto-flushed at 100 ops).
JSON documents are queried using Expression predicates. The JsonExpression class provides:
| Expression | Description |
|---|---|
je["Name"] == "Alice" |
Property equality |
je["Age"] > 20 |
Numeric comparison |
je["Age"] > 20 && je["City"] == "Seattle" |
Logical AND |
je["Active"] == true |
Boolean comparison |
je.GetSchema() == "Person" |
Filter by schema type |
je.GetKey() == "some-key" |
Filter by key |
When you need more control over indexing, key extraction, or nested property access, define a JsonSchema:
await db.SaveAsync(new JsonSchema
{
Name = "Person",
Properties = new()
{
new() { Name = "Name", Type = "string" },
new() { Name = "Age", Type = "integer" },
}
});JsonSchema entities are stored as objects in the database. On startup, all JsonSchema entities are loaded from Table Storage and their mappers initialized automatically.
Each property defines a field to extract, index, and make queryable:
| Field | Description | Default |
|---|---|---|
Name |
Lucene field name + Table Storage column name | Required |
Type |
"string", "integer", "number", or "boolean"
|
"string" |
JsonPath |
JSONPath for nested values (e.g. "$.address.city") |
Property name |
The Key field specifies how to extract the document key:
new JsonSchema { Name = "Person", Key = "id" } // top-level property
new JsonSchema { Name = "Person", Key = "$.user.id" } // nested via JSONPathKeyMode.Auto (default) generates a ULID when the key is missing. KeyMode.Manual requires the caller to provide it.
The Match property lets LottaDB automatically classify JSON documents on save -- no need to tag documents manually. When a document matches a discriminator expression, that schema's indexing rules apply automatically:
await db.SaveAsync(new JsonSchema
{
Name = "Person",
Match = "$.type == 'person'", // auto-classify when type == "person"
Properties = new()
{
new() { Name = "Name", Type = "string" },
new() { Name = "Age", Type = "integer" },
}
});
await db.SaveAsync(new JsonSchema
{
Name = "Company",
Match = "$.type == 'company'", // auto-classify when type == "company"
Properties = new()
{
new() { Name = "Name", Type = "string" },
new() { Name = "Industry", Type = "string" },
}
});Now documents are auto-classified on save based on their content:
// These are auto-classified as "Person" and "Company" based on $.type
await db.SaveAsync(JsonDocument.Parse("""{ "type": "person", "Name": "Alice", "Age": 30 }"""));
await db.SaveAsync(JsonDocument.Parse("""{ "type": "company", "Name": "Contoso", "Industry": "Tech" }"""));
// Search by schema type
var people = db.Search(je => je.GetSchema() == "Person").ToList();
var companies = db.Search(je => je.GetSchema() == "Company").ToList();The discriminator uses JSONPath equality syntax:
-
"$.type == 'person'"-- match whentypeequals"person" -
"$.category != 'draft'"-- match whencategoryis not"draft"
Resolution order when saving a JsonDocument:
- Explicit
doc.SetSchema("Person")-- if set, uses that schema - Discriminator matching -- first
JsonSchemawhoseMatchexpression matches - Default schema -- auto-queryable, no specific type
JsonSchema is a regular entity -- manage it with standard CRUD:
// List all schemas
await foreach (var schema in db.GetManyAsync<JsonSchema>()) { ... }
// Update -- adding a property triggers automatic reindex
await db.SaveAsync(new JsonSchema
{
Name = "Person",
Properties = new()
{
new() { Name = "Name", Type = "string" },
new() { Name = "Age", Type = "integer" },
new() { Name = "Email", Type = "string" }, // new!
}
});
// Delete -- removes mapper and Lucene index entries
await db.DeleteAsync<JsonSchema>("Person");The built-in On handler detects property changes and triggers a selective reindex -- only the affected type's Lucene entries are rebuilt.
| Storing POCOs | Storing JSON | |
|---|---|---|
| Definition | C# class (optional attributes) | JSON (optional JsonSchema) |
| Data type | POCO (T) |
JsonDocument |
| Registration | config.Store |
None (or db.SaveAsync(new JsonSchema {...})) |
| Auto-queryable | All simple-type properties | All top-level simple-type properties |
| Save | SaveAsync(entity) |
SaveAsync(doc) |
| Get | GetAsync |
GetAsync(key) |
| GetMany | GetManyAsync |
GetManyAsync(je => ...) |
| Search | Search |
Search(je => ...) |
| Delete | DeleteAsync |
DeleteAsync(key) |
| Query syntax | LINQ predicates + query strings |
JsonExpression predicates |
| Nested properties | Reflection on CLR properties | JsonPath (e.g. $.address.city) |
| Polymorphism | Base/derived types | Discriminator matching |
| Vector search | Supported | Supported |
| Schema changes | Rebuild on startup | Selective reindex at runtime |