LottaDB 1.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package LottaDB --version 1.0.0
                    
NuGet\Install-Package LottaDB -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="LottaDB" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="LottaDB" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="LottaDB" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add LottaDB --version 1.0.0
                    
#r "nuget: LottaDB, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package LottaDB@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=LottaDB&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=LottaDB&version=1.0.0
                    
Install as a Cake Tool

Logo

LottaDB

LottaDB is a .NET library that makes it easy to story any POCO in Azure Table Storage with full Lucene search, all with the goodness of LINQ.

  • One line to save
  • One line to search

Overview

LottaDB gives you a document database built on Azure Table Storage with automatic full-text search via Lucene Search Engine. Each LottaDB instance is a single table wih backing lucene catalog. Objects are stored with full POCO fidelity, while selected properties are promoted into Table Storage/Lucene for efficient querying using full typed Linq expressions.

Reactive handlers (On<T>) let you build materialized views, cascading updates, and side effects that run inline after each write.

Why LottaDB?

  • A lotta bang for a little buck. Table Storage is the cheapest durable storage in Azure. LottaDB adds Lucene so you get rich queries without the rich pricing.
  • A lotta LINQ. Query<T>() and Search<T>(), .Where(), .OrderBy() etc.
  • A lotta fidelity. Full JSON roundtrip. Lists, dictionaries, nested objects -- everything survives.
  • A lotta views. On<T> triggers build materialized views with plain C#. No event buses, no eventual consistency -- just inline code.
  • A lotta tenants. One instance per tenant. Natural isolation, simple backup, no noisy neighbors.
  • A lotta nothing to operate. Table Storage is serverless. Lucene runs in-process. No clusters, no connection pools, no ops team required.

Sweet spot

LottaDB is ideal for per-user or per-tenant workloads -- think user profiles, settings, activity feeds, personal knowledge bases, mailboxes, or per-project data. Thousands of objects per tenant, thousands of tenants per deployment. Each tenant gets its own isolated database for pennies/month. It's not designed for billion-row analytics or high-throughput write-heavy pipelines.

Installation

dotnet add package LottaDB

Features

  • Plain POCOs -- no base classes, no interfaces. Have an object? Store an object.
  • Full POCO roundtrip -- objects are serialized as JSON. Complex properties (lists, dictionaries, nested objects) survive storage and retrieval intact.
  • LINQ -- Rich Linq against typed objects makes it so easy.
  • Polymorphic queries -- Query<Base>()/Search<Base>() returns all derived types, correctly deserialized into their correct typed objects.
  • Triggers -- On<T> triggers run inline after saves/deletes with full DB access. Build your materialized views with plain C#.
  • Fluent or attribute configuration -- annotate your models, or configure foreign POCOs entirely via fluent API.
  • Per-tenant scaling -- one LottaDB instance per tenant.

Quick Example

// Define your model
public class Actor
{
    [Key]
    public string Username { get; set; } = "";

    [Queryable]
    public string DisplayName { get; set; } = "";

    public string AvatarUrl { get; set; } = "";
}

// Configure and create the database
var options = new LottaConfiguration();
options.Store<Actor>();

var db = new LottaDB("myapp", tableServiceClient, luceneDirectory, options);

// Save
await db.SaveAsync(new Actor { Username = "alice", DisplayName = "Alice" });

// Point read
var actor = await db.GetAsync<Actor>("alice");

// Query (Table Storage -- server-side filter on [Queryable] properties)
var results = db.Query<Actor>(a => a.DisplayName == "Alice")
    .ToList();

// Search (Lucene -- full-text search on [Queryable] properties)
var found = db.Search<Actor>()
    .Where(a => a.DisplayName == "Alice")
    .ToList();

Storing POCO objects

Lotta needs to know about types you want to store and the metadata about your type to store it and query it.

  • Key - the Key to store/retrieve objects under

  • Queryable - Promote a property to be queryable using Linq expressions

When you instantiate a DB you tell the data base about your type:

 var db = LottaDBFixture.CreateDb(opts =>
 {
     opts.Store<Actor>();
     opts.Store<Note>();
 });

Attribute-based modeling

If it's a POCO object you own you can add attributes to describe metadata on how to store the object in Lotta.

  • [Key] marks the unique identity property. Supports manual values or auto-generated ULIDs.
  • [Queryable] makes a property as queryable via Linq. .
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; } = "";

    public DateTimeOffset Published { get; set; }  // stored in JSON, not indexed
    public List<string> Tags { get; set; } = new(); // complex types just work
}

Fluent modeling

Lotta can store and retrieve POCO objects you don't own via fluent configuration.

  • SetKey() define the property which is the key for storage and retrieve
  • AddQueryable() - defines a property as queryable via Linq.
public class BareNote
{
    public string NoteId { get; set; } = "";
    public string AuthorId { get; set; } = "";
    public string Content { get; set; } = "";
}

 var db = LottaDBFixture.CreateDb(options =>
 {
    options.Store<BareNote>(s =>
    {
        s.SetKey(n => n.NoteId);
        s.AddQueryable(n => n.AuthorId).NotAnalyzed();
        s.AddQueryable(n => n.Content);  
    });
 }

Lotta Operations

Operation Description
SaveAsync<T>() Save T instance using Upsert semantics
ChangeAsync<T>() Apply changes to T instance via lamda
DeleteAsync<T>() Delete T instance object
QueryAsync<T>() Query against table storage for objects of type T
SearchAsync<T>() Search against lucene for objects of type T

SaveAsync<T>()

Save a POCO object into a Lotta DB using it's Key

await db.SaveAsync<Actor>(actor);

ChangeAsync<T>()

Apply change to T with ETag concurrency. It will fetch the object, call the lamda to change and attempt to save it with ETag concurrency. If the object fails, it will loop until it succeeds to mutate it.

await db.ChangeAsync<Actor>(key, actor =>
{
    actor.DisplayName = "Alice Updated";
    return actor;
});

DeleteAsync<T>()

Delete an object from a Lotta DB.

await db.DeleteAsync<Note>(key);
await db.DeleteAsync<Note>(note);

QueryAsync<T>()

Search table storage using linq

NOTE: only filter passed to QueryAsync is processed server side by table storage and it needs to be on [Queryable] properties. All other linq operations are processed client side after fetching the data from table storage.

foreach(var actor in db.QueryAsync<Actor>(actor => actor.Age > 50)
{
    ...
}

SearchAsync<T>()

Search lucene index using linq search syntax.

NOTE: only [Queryable] properties are searchable in lucene and string properties support full text search with Contains.

foreach(var actor in db.SearchAsync<Actor>("name:bob*")
                       .Where(actor => actor.Age > 50)
{
    ...
}

LINQ in Lotta

Lotta stores 2 representations of every object, one in table storage (for the truth), and one a Lucene index (for fast access). Query() gives you a Linq query over table storage and .Search() uses the Linq To Lucene library to query the search engine with linq expressions

Query (Table Storage)

Filters on [Queryable] properties are executed by table storage server-side.

// All actors
var all = db.Query<Actor>().ToList();

// Server-side filter (AuthorId is [Queryable])
var aliceNotes = db.Query<Note>(n => n.AuthorId == "alice")
    .ToList();

// Predicate shorthand
var aliceNotes = db.Query<Note>(n => n.AuthorId == "alice").ToList();

// Polymorphic query -- returns Person and Employee
var people = db.Query<Person>().ToList();

Search (Lucene)

Filters on [Queryable] properties are executed against Lucene catalog supporting full-text search with Contains.

// Full-text search
var results = db.Search<Note>()
    .Where(n => n.Content.Contains("lucene"))
    .ToList();

// Exact match on NotAnalyzed field
var active = db.Search<Note>()
    .Where(n => n.AuthorId == "alice")
    .ToList();

// FREETEXT query
var results = db.Search<Note>("foo bar").ToList();

// Lucene Query syntax
var results = db.Search<Note>("Title:foo AND bar").ToList();

Triggers via On<T>

You can have code run when an object is saved/changed/deleted. That trigger can create new objects for cascading views.

Triggers run inline after each save or delete. They receive the object, the trigger kind (Saved or Deleted), and the full DB instance.

options.On<Note>(async (note, kind, db) =>
{
    Console.WriteLine($"Note {note.NoteId} was {kind}");
});

Materialized views via On<T>

You can use On<T> triggers to build derived objects that stay in sync automatically, whenever the trigger runs you can create/update/delete other objects.

public class NoteView
{
    [Key]
    public string Id { get; set; } = "";

    [Queryable(QueryableMode.NotAnalyzed)]
    public string NoteId { get; set; } = "";

    [Queryable]
    public string AuthorDisplay { get; set; } = "";

    [Queryable]
    public string Content { get; set; } = "";
}

options.On<Note>(async (note, kind, db) =>
{
    // maintain NoteView as materialized view that's updated when Note changes.
    if (kind == TriggerKind.Deleted)
    {
        await db.DeleteAsync<NoteView>(nv => nv.NoteId == note.NoteId);
        return;
    }

    var actor = await db.GetAsync<Actor>(note.AuthorId);
    await db.SaveAsync(new NoteView
    {
        Id = $"nv-{note.NoteId}",
        NoteId = note.NoteId,
        AuthorDisplay = actor?.DisplayName ?? "",
        Content = note.Content,
    });
});

Handlers can trigger further handlers (cascading views). Cycle detection prevents infinite loops -- if the same type appears twice in a handler chain, processing stops.

Error handling

Handler errors never block the source save/delete. They are captured in ObjectResult.Errors:

var result = await db.SaveAsync(note);
if (result.Errors.Count > 0)
{
    // log handler failures
}
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on LottaDB:

Package Downloads
LottaDB.Tiki

Tiki.Net integration for LottaDB — auto-extract rich metadata from blobs on upload.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.1.0 37 5/16/2026
3.0.0 42 5/13/2026
2.0.1 85 5/5/2026
2.0.0 106 5/5/2026
1.1.0 96 4/24/2026
1.0.2 99 4/16/2026
1.0.1 92 4/16/2026
1.0.0 95 4/16/2026