-
Notifications
You must be signed in to change notification settings - Fork 0
Storing POCOs
LottaDB can store any C# class (POCO)
By default, LottaDB automatically promotes all simple-type top-level properties (string, int, long, double, bool, DateTime, Guid) to queryable columns indexed in Lucene. If no [Key] property is defined, a ULID is auto-generated as the key.
The simplest way to get started -- just define a plain class and store it:
public class Actor
{
public string Username { get; set; } = "";
public string DisplayName { get; set; } = "";
public int FollowerCount { get; set; }
}
var db = await catalog.GetDatabaseAsync("mydb");
// A ULID key is auto-generated. All properties are queryable automatically.
await db.SaveAsync(new Actor { Username = "alice", DisplayName = "Alice", FollowerCount = 42 });
var results = db.Search<Actor>().Where(a => a.Username == "alice").ToList();No [Key], no [Queryable] -- AutoQueryable handles it. Attributes are available for fine-tuning when you need more control.
Tell LottaDB about your types when using attributes to fine tune behavior creating a database:
var db = await catalog.GetDatabaseAsync("mydb", config =>
{
config.Store<Actor>();
config.Store<Note>();
});Attributes give you fine control over storage and indexing behavior:
-
[Key]marks the unique identity property. Supports manual values or auto-generated ULIDs. -
[Queryable]makes a property queryable via LINQ, indexed in Lucene, and promoted to a Table Storage column. (With AutoQueryable on, simple-type properties are already queryable -- use[Queryable]to set specific modes likeNotAnalyzedorVector.) -
[NotQueryable]excludes a property from AutoQueryable promotion. Use on large string properties that would exceed Azure Table Storage's 64KB column limit. -
[DefaultSearch](class-level) sets which property is the default target for free-text queries.
| Attribute | Target | Purpose |
|---|---|---|
[Key] |
Property | Designates the key property. Without it, a ULID is auto-generated. |
[Queryable] |
Property | Marks a property as queryable with optional mode (NotAnalyzed, Vector). |
[NotQueryable] |
Property | Excludes a property from AutoQueryable promotion. |
[DefaultSearch] |
Class | Sets the default free-text search property. |
public class Note
{
[Key]
public string NoteId { get; set; } = "";
[Queryable(QueryableMode.NotAnalyzed)] // exact match
public string AuthorId { get; set; } = "";
[Queryable] // full-text search (string default)
public string Content { get; set; } = "";
[Queryable(Vector = true)] // full-text + vector similarity search
public string Summary { get; set; } = "";
[NotQueryable] // too large for a Table Storage column
public string RawHtml { get; set; } = "";
public DateTimeOffset Published { get; set; } // auto-indexed by AutoQueryable
public List<string> Tags { get; set; } = new(); // complex types stored in JSON, not indexed
}For POCO classes you don't own, use fluent configuration:
config.Store<BareNote>(s =>
{
s.SetKey(n => n.NoteId);
s.AddQueryable(n => n.AuthorId).NotAnalyzed();
s.AddQueryable(n => n.Content);
});AutoQueryable is on by default for all registered types. It automatically promotes every public property of a simple type (string, int, long, double, bool, DateTime, Guid, and their nullable forms) to a queryable column in Table Storage and an indexed field in Lucene.
Properties that already have [Queryable] keep their explicit configuration. Properties marked [NotQueryable] are skipped. Complex types (lists, dictionaries, nested objects) are stored in JSON but not indexed.
To disable AutoQueryable for a type, use the fluent API:
config.Store<Note>(s =>
{
s.AutoQueryable(false); // only index explicitly configured properties
s.AddQueryable(n => n.AuthorId).NotAnalyzed();
s.AddQueryable(n => n.Content);
});// Save
await db.SaveAsync(new Actor { Username = "alice", DisplayName = "Alice" });
// Get
var actor = await db.GetAsync<Actor>("alice");
// GetMany with server-side filter
await foreach (var note in db.GetManyAsync<Note>(n => n.AuthorId == "alice")) { ... }
// Search (Lucene full-text)
var results = db.Search<Note>("lucene").ToList();
var results = db.Search<Note>(n => n.Content.Contains("lucene")).ToList();
// Delete
await db.DeleteAsync<Note>("note-123");
// Bulk save
await db.SaveManyAsync([actor1, actor2, note1]);
// Optimistic concurrency (read-modify-write)
await db.ChangeAsync<Actor>("alice", a => a.Counter++);All returned objects are annotated with key, ETag, and cached JSON via Object Metadata.
Register a base type and its derived types. Queries on the base type return all subtypes:
config.Store<Person>();
config.Store<Employee>(); // extends Person
// Returns both Person and Employee instances
var people = db.GetManyAsync<Person>().ToList();
// Search also returns subtypes
var results = db.Search<Person>("department:engineering").ToList();By default, LottaDB creates a synthetic _content_ field that concatenates all analyzed string properties for free-text search. You can override this with [DefaultSearch]:
Attribute-based:
[DefaultSearch(nameof(Content))]
public class Article
{
[Key]
public string Id { get; set; } = "";
[Queryable]
public string Title { get; set; } = "";
[Queryable]
public string Body { get; set; } = "";
[Queryable(Vector = true)]
public string Content { get => $"{Title} {Body}"; } // composed search field
}Fluent:
config.Store<Article>(s =>
{
s.SetKey(a => a.Id);
s.AddQueryable(a => a.Title);
s.AddQueryable(a => a.Body);
s.AddQueryable(a => a.Content).Vector();
s.DefaultSearch(a => a.Content);
});Now Search<Article>("lucene") targets Content, while a.Title.Query("lucene") still targets Title directly.
Register handlers that run after save or delete:
config.On<Note>(async (note, kind, db) =>
{
if (kind == TriggerKind.Saved)
Console.WriteLine($"Note saved: {note.NoteId}");
});See Triggers and Views for materialized views and cascading handlers.