SoloDB 0.2.0
See the version list below for details.
dotnet add package SoloDB --version 0.2.0
NuGet\Install-Package SoloDB -Version 0.2.0
<PackageReference Include="SoloDB" Version="0.2.0" />
<PackageVersion Include="SoloDB" Version="0.2.0" />
<PackageReference Include="SoloDB" />
paket add SoloDB --version 0.2.0
#r "nuget: SoloDB, 0.2.0"
#addin nuget:?package=SoloDB&version=0.2.0
#tool nuget:?package=SoloDB&version=0.2.0
SoloDB
SoloDB is a light, fast and robust NoSQL and SQL embedded .NET database built on top of SQLite using the JSONB data type.
Features
Imagine the power of MongoDB and SQL combined.
- SQLite at the core.
- Serverless, it is a .NET library.
- Simple API, similar to MongoDB, see the below.
- Thread safe using a connection pool.
- ACID with full transaction support.
- File System for large files storage.
- Support for polymorphic types.
- Reliable with a WAL log file.
- Support for indexes for fast search.
- Full LINQ and IQueryable support.
- MongoDB inspired Custom ID Generation.
- Direct SQL support.
- Open source.
- .NET Standard 2.0 and 2.1
- Pretty well tested: 600+ of tests, but in the tradition of SQLite, we keep them private.
How to install
From NuGet
dotnet add package SoloDB
Usage
Initializing the Database
You can specify either a file path or an in-memory database.
using SoloDatabase;
using var onDiskDB = new SoloDB("path/to/database.db");
using var inMemoryDB = new SoloDB("memory:database-name");
Creating and Accessing Collections
public class User
{
// Any int64 'Id' property is automatically synced with the SQLite's primary key.
public long Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
using var db = new SoloDB("memory:my-app");
// Get a strongly-typed collection
var users = db.GetCollection<User>();
// Get an untyped collection (useful for dynamic scenarios)
var untypedUsers = db.GetUntypedCollection("User");
Custom ID Generation
using SoloDatabase.Attributes;
using SoloDatabase.Types;
using System.Linq;
public class MyStringIdGenerator : IIdGenerator<MyCustomIdType>
{
public object GenerateId(ISoloDBCollection<MyCustomIdType> col, MyCustomIdType item)
{
var lastItem = col.OrderByDescending(x => long.Parse(x.Id)).FirstOrDefault();
long maxId = (lastItem == null) ? 0 : long.Parse(lastItem.Id);
return (maxId + 1).ToString();
}
public bool IsEmpty(object id) => string.IsNullOrEmpty(id as string);
}
public class MyCustomIdType
{
[SoloId(typeof(MyStringIdGenerator))]
public string Id { get; set; }
public string Data { get; set; }
}
var customIdCollection = db.GetCollection<MyCustomIdType>();
var newItem = new MyCustomIdType { Data = "Custom ID Test" };
customIdCollection.Insert(newItem); // newItem.Id will be populated by MyStringIdGenerator
System.Console.WriteLine($"Generated ID: {newItem.Id}");
Indexing Documents
using SoloDatabase.Attributes;
public class IndexedProduct
{
public long Id { get; set; } // Implicitly indexed by SoloDB
[Indexed(/* unique = */ true)] // Create a unique index on SKU
public string SKU { get; set; }
[Indexed(false)] // Create a non-unique index on Category
public string Category { get; set; }
public decimal Price { get; set; }
}
// ...
var products = db.GetCollection<IndexedProduct>();
products.Insert(new IndexedProduct { SKU = "BOOK-123", Category = "Books", Price = 29.99m });
// Verify unique index constraint. This will throw a unique constraint violation exception.
try
{
products.Insert(new IndexedProduct { SKU = "BOOK-123", Category = "Fiction", Price = 19.99m });
}
catch (Microsoft.Data.Sqlite.SqliteException ex)
{
System.Console.WriteLine($"Successfully caught expected exception: {ex.Message}");
}
// Test querying with indexes
var book = products.FirstOrDefault(p => p.SKU == "BOOK-123");
System.Console.WriteLine($"Found book with SKU BOOK-123: Price {book.Price}");
products.Insert(new IndexedProduct { SKU = "BOOK-456", Category = "Books", Price = 14.99m });
var booksInCategory = products.Where(p => p.Category == "Books").ToList();
System.Console.WriteLine($"Found {booksInCategory.Count} books in the 'Books' category.");
Transactions
Use the WithTransaction
method to execute a function within a transaction.
try
{
db.WithTransaction(tx => {
var collection = tx.GetCollection<ulong>();
// Perform operations within the transaction.
collection.Insert(420);
throw new System.OperationCanceledException("Simulating a rollback."); // Simulate a fail.
});
} catch (System.OperationCanceledException) {}
System.Console.WriteLine($"Collection exists after rollback: {db.CollectionExists<ulong>()}"); // False
Polymorphic Types
public abstract class Shape
{
public long Id { get; set; }
public string Color { get; set; }
public abstract double CalculateArea();
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double CalculateArea() => System.Math.PI * Radius * Radius;
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double CalculateArea() => Width * Height;
}
// ...
var shapes = db.GetCollection<Shape>(); // Store as the base type 'Shape'
shapes.Insert(new Circle { Color = "Red", Radius = 5.0 });
shapes.Insert(new Rectangle { Color = "Blue", Width = 4.0, Height = 6.0 });
// Get all circles
var circles = shapes.OfType<Circle>().ToList();
foreach (var circle in circles)
{
System.Console.WriteLine($"Red Circle - Radius: {circle.Radius}, Area: {circle.CalculateArea()}");
}
// Get all shapes with Color "Blue"
var blueShapes = shapes.Where(s => s.Color == "Blue").ToList();
foreach (var shape in blueShapes)
{
if (shape is Rectangle rect)
{
System.Console.WriteLine($"Blue Rectangle - Width: {rect.Width}, Height: {rect.Height}, Area: {rect.CalculateArea()}");
}
}
Direct SQLite access using the build-in SoloDatabase.SQLiteTools.IDbConnectionExtensions.
using static SoloDatabase.SQLiteTools.IDbConnectionExtensions;
...
using var pooledConnection = db.Connection.Borrow();
pooledConnection.Execute(
"CREATE TABLE Users (Id INTEGER PRIMARY KEY, Name TEXT, Age INTEGER)");
var insertSql = "INSERT INTO Users (Name, Age) VALUES (@Name, @Age) RETURNING Id;";
var userId = pooledConnection.QueryFirst<long>(insertSql, new { Name = "John Doe", Age = 30 });
Assert.IsTrue(userId > 0, "Failed to insert new user and get a valid ID.");
var queriedAge = pooledConnection.QueryFirst<int>("SELECT Age FROM Users WHERE Name = 'John Doe'");
Assert.AreEqual(30, queriedAge);
Backing Up the Database
You can create a backup of the database using the BackupTo
or VacuumTo
methods.
db.BackupTo(otherDb);
db.VacuumTo("path/to/backup.db");
Optimizing the Database
The Optimize
method can optimize the database using statistically information, it runs automatically on startup.
db.Optimize();
File storage
using SoloDatabase;
using SoloDatabase.FileStorage;
using System.IO;
using System.Text;
var fs = db.FileSystem;
var randomBytes = new byte[256];
System.Random.Shared.NextBytes(randomBytes);
// Create a directory and set metadata
var directory = fs.GetOrCreateDirAt("/my_documents/reports");
fs.SetDirectoryMetadata(directory, "Sensitivity", "Confidential");
var dirInfo = fs.GetDirAt("/my_documents/reports");
System.Console.WriteLine($"Directory '/my_documents/reports' metadata 'Sensitivity': {dirInfo.Metadata["Sensitivity"]}");
// Upload a file and set metadata
string filePath = "/my_documents/reports/annual_report.txt";
using (var ms = new System.IO.MemoryStream(System.Text.Encoding.UTF8.GetBytes("This is a test report.")))
{
fs.Upload(filePath, ms);
}
fs.SetMetadata(filePath, "Author", "Jane Doe");
fs.SetFileCreationDate(filePath, System.DateTimeOffset.UtcNow.AddDays(-7));
var fileInfo = fs.GetAt(filePath);
System.Console.WriteLine($"File '{fileInfo.Name}' author: {fileInfo.Metadata["Author"]}");
// Write at a specific offset (sparse file)
string sparseFilePath = "/my_documents/sparse_file.dat";
fs.WriteAt(sparseFilePath, 1024 * 1024, randomBytes); // Write at 1MB offset
var readData = fs.ReadAt(sparseFilePath, 1024 * 1024, randomBytes.Length);
System.Console.WriteLine($"Sparse file write/read successful: {System.Linq.Enumerable.SequenceEqual(randomBytes, readData)}");
// Download file content
using (var targetStream = new System.IO.MemoryStream())
{
fs.Download(filePath, targetStream);
targetStream.Position = 0;
string content = new System.IO.StreamReader(targetStream).ReadToEnd();
System.Console.WriteLine($"Downloaded content: {content}");
}
// Recursively list entries
var entries = fs.RecursiveListEntriesAt("/my_documents");
System.Console.WriteLine($"Found {entries.Count} entries recursively under /my_documents.");
// Move a file to a new location (and rename it)
fs.MoveFile(filePath, "/archive/annual_report_2023.txt");
bool originalExists = fs.Exists(filePath); // false
bool newExists = fs.Exists("/archive/annual_report_2023.txt"); // true
System.Console.WriteLine($"Original file exists after move: {originalExists}. New file exists: {newExists}");
// Bulk upload multiple files
var bulkFiles = new System.Collections.Generic.List<SoloDatabase.FileStorage.BulkFileData>
{
// The constructor allows setting path, data, and optional timestamps.
new("/bulk_uploads/file1.log", System.Text.Encoding.UTF8.GetBytes("Log entry 1"), null, null),
new("/bulk_uploads/images/pic.jpg", randomBytes, null, null)
};
fs.UploadBulk(bulkFiles);
System.Console.WriteLine($"Bulk upload successful. File exists: {fs.Exists("/bulk_uploads/images/pic.jpg")}");
// Use SoloFileStream for controlled writing
using (var fileStream = fs.OpenOrCreateAt("/important_data/critical.bin"))
{
fileStream.Write(randomBytes, 0, 10);
}
var criticalFileInfo = fs.GetAt("/important_data/critical.bin");
System.Console.WriteLine($"SoloFileStream created file with size: {criticalFileInfo.Size} and a valid hash.");
Example Usage
Here is an example of how to use SoloDB to manage a collection of documents in C#:
SoloDB
using SoloDatabase;
using SoloDatabase.Attributes;
using System.Linq;
public class MyDataType
{
public long Id { get; set; }
[Indexed(/* unique = */ false)]
public string Name { get; set; }
public string Data { get; set; }
}
// using var db = new SoloDB(...);
var collection = db.GetCollection<MyDataType>();
// Insert a document
var newDoc = new MyDataType { Name = "Document 1", Data = "Some data" };
collection.Insert(newDoc); // Id will be auto-generated and set on newDoc.Id
System.Console.WriteLine($"Inserted document with ID: {newDoc.Id}");
// If the Id property does not exist, then you can use the return value of Insert.
var dataToInsert = new MyDataType { Name = "Document 2", Data = "More data" };
var docId = collection.Insert(dataToInsert);
System.Console.WriteLine($"Inserted document, ID from object: {docId}");
// Query all documents into a C# list
var allDocuments = collection.ToList();
System.Console.WriteLine($"Total documents: {allDocuments.Count}");
var documentsData = collection.Where(d => d.Name.StartsWith("Document"))
.Select(d => d.Data)
.ToList();
// Update a document
var docToUpdate = collection.GetById(docId);
docToUpdate.Data = "Updated data for Document 2";
collection.Update(docToUpdate);
// Verify the update
var updatedDoc = collection.GetById(docId);
System.Console.WriteLine($"Updated data: {updatedDoc.Data}"); // "Updated data for Document 2"
// Delete the first document by its primary key
int deleteCount = collection.Delete(docId);
System.Console.WriteLine($"Documents deleted: {deleteCount}"); // 1
// Verify the final count
System.Console.WriteLine($"Final document count: {collection.Count()}"); // 1
And a simple one in F#:
SoloDB
[<CLIMutable>]
type MyType = { Id: int64; Name: string; Data: string }
use db = new SoloDB("./mydatabase.db")
let collection = db.GetCollection<MyType>()
// Insert a document
let docId = collection.Insert({ Id = 0; Name = "Document 1"; Data = "Some data" })
// Or
let data = { Id = 0; Name = "Document 1"; Data = "Some data" }
collection.Insert(data) |> ignore
printfn "%A" data.Id // 2
// Query all documents into a F# list
let documents = collection.ToList()
// Query the Data property, where Name starts with 'Document'
let documentsData = collection.Where(fun d -> d.Name.StartsWith "Document").Select(fun d -> d.Data).ToList()
let data = {data with Data = "Updated data"}
// Update a document
collection.Update(data)
// Delete a document
let count = collection.Delete(data.Id) // 1
Licence
This project is licensed under LGPL-3.0, with the additional permission to distribute applications that incorporate an unmodified DLL of this library in Single-file deployment, Native AOT, and other bundling technologies that embed the library into the executable.
FAQ
Why create this project?
- For fun and profit, and to have a more simple alternative to MongoDB with the reliability of SQLite.
Footnote
API is subject to change.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. 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. |
.NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.0 is compatible. netstandard2.1 is compatible. |
.NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen40 was computed. tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- FSharp.Core (>= 9.0.300)
- Microsoft.Data.Sqlite (>= 8.0.6)
- Portable.System.DateTimeOnly (>= 8.0.1)
- Snappier (>= 1.2.0)
- SQLitePCLRaw.bundle_e_sqlite3 (>= 2.1.8)
- System.Data.SqlClient (>= 4.8.6)
-
.NETStandard 2.1
- FSharp.Core (>= 9.0.300)
- Microsoft.Data.Sqlite (>= 8.0.6)
- Portable.System.DateTimeOnly (>= 8.0.1)
- Snappier (>= 1.2.0)
- SQLitePCLRaw.bundle_e_sqlite3 (>= 2.1.8)
- System.Data.SqlClient (>= 4.8.6)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last Updated |
---|---|---|
0.4.0 | 34 | 7/5/2025 |
0.3.2 | 79 | 6/28/2025 |
0.3.1 | 132 | 6/25/2025 |
0.3.0 | 93 | 6/21/2025 |
0.2.2 | 111 | 6/7/2025 |
0.2.1 | 84 | 6/6/2025 |
0.2.0 | 139 | 6/5/2025 |
0.1.9 | 138 | 6/4/2025 |
0.1.8 | 139 | 6/4/2025 |
0.1.7 | 134 | 6/4/2025 |
0.1.6 | 137 | 6/2/2025 |
0.1.5 | 137 | 6/2/2025 |
0.1.3 | 105 | 5/31/2025 |
0.1.2 | 99 | 5/31/2025 |
0.1.1 | 95 | 5/31/2025 |
0.1.0 | 96 | 5/31/2025 |
0.0.27 | 118 | 1/30/2025 |
0.0.26 | 105 | 1/28/2025 |
0.0.25 | 104 | 1/23/2025 |
0.0.24 | 100 | 1/23/2025 |
0.0.23 | 102 | 1/23/2025 |
0.0.22 | 96 | 1/23/2025 |
0.0.21 | 98 | 1/18/2025 |
0.0.20 | 122 | 9/14/2024 |
0.0.19 | 116 | 9/14/2024 |
0.0.18 | 109 | 9/14/2024 |
0.0.17 | 113 | 8/30/2024 |
0.0.16 | 142 | 8/22/2024 |
0.0.15 | 142 | 8/13/2024 |
0.0.14 | 95 | 8/6/2024 |
0.0.13 | 98 | 8/4/2024 |
0.0.12 | 100 | 8/2/2024 |
0.0.11 | 86 | 7/31/2024 |
0.0.10 | 106 | 7/28/2024 |
0.0.9 | 95 | 7/28/2024 |
0.0.8 | 103 | 7/27/2024 |
0.0.7 | 103 | 7/26/2024 |
0.0.6 | 125 | 7/23/2024 |
0.0.5 | 113 | 7/10/2024 |
0.0.4 | 128 | 7/8/2024 |