This guide will get you up and running with WitDatabase in minutes. We'll start with the simplest examples and progressively add more features.
Prerequisites: Make sure you've installed the appropriate WitDatabase package (see Installation.
What you'll learn:
- Store and retrieve data with the Core key-value API
- Execute SQL queries with ADO.NET
- Use Entity Framework Core for object-relational mapping
- Secure your database with encryption
- Ensure data integrity with transactions
Your First Database
Let's start with the simplest possible database — no SQL, no configuration, just storing and retrieving data.
In-Memory Database
The fastest way to experiment with WitDatabase is using an in-memory database. Data exists only while the database is open — perfect for testing and learning.
using OutWit.Database.Core.Builder;
// Create an in-memory database
using var db = WitDatabase.CreateInMemory();
// Store a value
var key = "greeting"u8.ToArray();
var value = "Hello, WitDatabase!"u8.ToArray();
db.Put(key, value);
// Retrieve the value
var retrieved = db.Get(key);
Console.WriteLine(System.Text.Encoding.UTF8.GetString(retrieved!));
// Output: Hello, WitDatabase!
What's happening here?
WitDatabase.CreateInMemory()— creates a database that lives entirely in RAM"greeting"u8.ToArray()— converts a string to a UTF-8 byte array (C# 11+ syntax)db.Put(key, value)— stores the key-value pairdb.Get(key)— retrieves the value (returnsnullif not found)
Note: The
u8suffix creates aReadOnlySpan<byte>from a string literal. We call.ToArray()because WitDatabase storesbyte[]arrays.
File-Based Database
To persist data between application runs, use a file-based database:
using OutWit.Database.Core.Builder;
// Create a new database file (or open existing)
using var db = WitDatabase.CreateOrOpen("myapp.witdb");
// Store data
db.Put("user:1:name"u8.ToArray(), "Alice"u8.ToArray());
db.Put("user:1:email"u8.ToArray(), "alice@example.com"u8.ToArray());
db.Put("user:2:name"u8.ToArray(), "Bob"u8.ToArray());
db.Put("user:2:email"u8.ToArray(), "bob@example.com"u8.ToArray());
Console.WriteLine("Data saved to myapp.witdb");
Static factory methods:
| Method | Description |
|---|---|
WitDatabase.Create(path) |
Create new database (fails if exists) |
WitDatabase.Open(path) |
Open existing database (fails if not exists) |
WitDatabase.CreateOrOpen(path) |
Create new or open existing (most common) |
WitDatabase.CreateInMemory() |
Create in-memory database |
Basic Key-Value Operations
WitDatabase at its core is a key-value store. Here are the fundamental operations:
using OutWit.Database.Core.Builder;
using System.Text;
using var db = WitDatabase.CreateOrOpen("demo.witdb");
// Helper to convert strings to bytes
byte[] ToBytes(string s) => Encoding.UTF8.GetBytes(s);
string FromBytes(byte[] b) => Encoding.UTF8.GetString(b);
// ═══════════════════════════════════════════════════════════
// PUT — Store a value
// ═══════════════════════════════════════════════════════════
db.Put(ToBytes("name"), ToBytes("Alice"));
db.Put(ToBytes("age"), ToBytes("30"));
db.Put(ToBytes("city"), ToBytes("New York"));
// ═══════════════════════════════════════════════════════════
// GET — Retrieve a value
// ═══════════════════════════════════════════════════════════
var name = db.Get(ToBytes("name"));
if (name != null)
{
Console.WriteLine(
Loading...
quot;Name: {FromBytes(name)}"); // Name: Alice
}
// Check if key exists
var unknown = db.Get(ToBytes("unknown_key"));
Console.WriteLine(
Loading...
quot;Unknown key exists: {unknown != null}"); // false
// ═══════════════════════════════════════════════════════════
// DELETE — Remove a value
// ═══════════════════════════════════════════════════════════
db.Delete(ToBytes("city"));
var city = db.Get(ToBytes("city"));
Console.WriteLine(
Loading...
quot;City after delete: {city != null}"); // false
// ═══════════════════════════════════════════════════════════
// UPDATE — Put overwrites existing values
// ═══════════════════════════════════════════════════════════
db.Put(ToBytes("age"), ToBytes("31")); // Happy birthday!
var newAge = db.Get(ToBytes("age"));
Console.WriteLine(
Loading...
quot;New age: {FromBytes(newAge!)}"); // 31
// ═══════════════════════════════════════════════════════════
// EXISTS — Check without retrieving
// ═══════════════════════════════════════════════════════════
bool exists = db.Exists(ToBytes("name"));
Console.WriteLine(
Loading...
quot;Name exists: {exists}"); // true
Scanning Keys
You can iterate over a range of keys using Scan:
using var db = WitDatabase.CreateOrOpen("users.witdb");
// Store some users with prefixed keys
db.Put("user:001:name"u8.ToArray(), "Alice"u8.ToArray());
db.Put("user:001:email"u8.ToArray(), "alice@example.com"u8.ToArray());
db.Put("user:002:name"u8.ToArray(), "Bob"u8.ToArray());
db.Put("user:002:email"u8.ToArray(), "bob@example.com"u8.ToArray());
db.Put("user:003:name"u8.ToArray(), "Charlie"u8.ToArray());
db.Put("config:theme"u8.ToArray(), "dark"u8.ToArray());
// Scan all keys starting with "user:"
var startKey = "user:"u8.ToArray();
var endKey = "user:\xff"u8.ToArray(); // \xff ensures we get all "user:*" keys
Console.WriteLine("All user keys:");
foreach (var (key, value) in db.Scan(startKey, endKey))
{
var keyStr = System.Text.Encoding.UTF8.GetString(key);
var valueStr = System.Text.Encoding.UTF8.GetString(value);
Console.WriteLine(
Loading...
quot; {keyStr} = {valueStr}");
}
// Output:
// user:001:email = alice@example.com
// user:001:name = Alice
// user:002:email = bob@example.com
// user:002:name = Bob
// user:003:name = Charlie
Key design tips:
- Use prefixes to group related data:
user:, order:, config:
- Use separators for hierarchy:
user:123:profile:name
- Keys are sorted lexicographically — design keys with this in mind
- Keep keys short — they're stored with every value
Async Operations
All operations have async versions for non-blocking I/O:
using var db = WitDatabase.CreateOrOpen("async-demo.witdb");
// Async put
await db.PutAsync("key1"u8.ToArray(), "value1"u8.ToArray());
// Async get
var value = await db.GetAsync("key1"u8.ToArray());
// Async delete
await db.DeleteAsync("key1"u8.ToArray());
// Async scan
await foreach (var (key, val) in db.ScanAsync(startKey, endKey))
{
// Process key-value pair
}
Working with Complex Data
For storing objects, serialize them to bytes. Here's an example with JSON:
using System.Text.Json;
using OutWit.Database.Core.Builder;
using var db = WitDatabase.CreateOrOpen("objects.witdb");
// Define a class
public record User(int Id, string Name, string Email, DateTime CreatedAt);
// Serialize to JSON bytes
var user = new User(1, "Alice", "alice@example.com", DateTime.UtcNow);
var key =
Loading...
quot;user:{user.Id}"u8.ToArray();
var value = JsonSerializer.SerializeToUtf8Bytes(user);
db.Put(key, value);
// Deserialize back
var retrieved = db.Get(key);
var loadedUser = JsonSerializer.Deserialize<User>(retrieved!);
Console.WriteLine(
Loading...
quot;Loaded: {loadedUser!.Name} ({loadedUser.Email})");
Tip: For better performance, consider using MessagePack or MemoryPack instead of JSON.
Complete Example: Simple Note Store
Let's build a simple note storage application:
using System.Text;
using System.Text.Json;
using OutWit.Database.Core.Builder;
// ═══════════════════════════════════════════════════════════
// Simple Note Store using WitDatabase Core API
// ═══════════════════════════════════════════════════════════
using var db = WitDatabase.CreateOrOpen("notes.witdb");
// Data model
record Note(string Id, string Title, string Content, DateTime CreatedAt, DateTime? UpdatedAt);
// Helper functions
string NewId() => Guid.NewGuid().ToString("N")[..8];
byte[] NoteKey(string id) => Encoding.UTF8.GetBytes(
Loading...
quot;note:{id}");
byte[] ToJson(Note note) => JsonSerializer.SerializeToUtf8Bytes(note);
Note FromJson(byte[] data) => JsonSerializer.Deserialize<Note>(data)!;
// ─────────────────────────────────────────────────────────────
// Create some notes
// ─────────────────────────────────────────────────────────────
var note1 = new Note(NewId(), "Shopping List", "Milk, Bread, Eggs", DateTime.UtcNow, null);
var note2 = new Note(NewId(), "Meeting Notes", "Discuss Q3 roadmap", DateTime.UtcNow, null);
var note3 = new Note(NewId(), "Ideas", "Build a note-taking app!", DateTime.UtcNow, null);
db.Put(NoteKey(note1.Id), ToJson(note1));
db.Put(NoteKey(note2.Id), ToJson(note2));
db.Put(NoteKey(note3.Id), ToJson(note3));
Console.WriteLine(
Loading...
quot;Created 3 notes");
// ─────────────────────────────────────────────────────────────
// List all notes
// ─────────────────────────────────────────────────────────────
Console.WriteLine("\nAll notes:");
var start = "note:"u8.ToArray();
var end = "note:\xff"u8.ToArray();
foreach (var (key, value) in db.Scan(start, end))
{
var note = FromJson(value);
Console.WriteLine(
Loading...
quot; [{note.Id}] {note.Title}");
}
// ─────────────────────────────────────────────────────────────
// Read a specific note
// ─────────────────────────────────────────────────────────────
var data = db.Get(NoteKey(note1.Id));
if (data != null)
{
var loaded = FromJson(data);
Console.WriteLine(
Loading...
quot;\nNote details:");
Console.WriteLine(
Loading...
quot; Title: {loaded.Title}");
Console.WriteLine(
Loading...
quot; Content: {loaded.Content}");
Console.WriteLine(
Loading...
quot; Created: {loaded.CreatedAt}");
}
// ─────────────────────────────────────────────────────────────
// Update a note
// ─────────────────────────────────────────────────────────────
var updated = note1 with
{
Content = "Milk, Bread, Eggs, Cheese",
UpdatedAt = DateTime.UtcNow
};
db.Put(NoteKey(updated.Id), ToJson(updated));
Console.WriteLine(
Loading...
quot;\nUpdated note: {updated.Title}");
// ─────────────────────────────────────────────────────────────
// Delete a note
// ─────────────────────────────────────────────────────────────
db.Delete(NoteKey(note3.Id));
Console.WriteLine(
Loading...
quot;Deleted note: {note3.Title}");
// ─────────────────────────────────────────────────────────────
// Count remaining notes
// ─────────────────────────────────────────────────────────────
int count = 0;
foreach (var _ in db.Scan(start, end))
count++;
Console.WriteLine(
Loading...
quot;\nTotal notes: {count}");
Output:
Created 3 notes
All notes:
[a1b2c3d4] Shopping List
[e5f6g7h8] Meeting Notes
[i9j0k1l2] Ideas
Note details:
Title: Shopping List
Content: Milk, Bread, Eggs
Created: 2025-01-20T12:00:00Z
Updated note: Shopping List
Deleted note: Ideas
Total notes: 2
When to Use Core API vs SQL
Use Core API when...
Use SQL (ADO.NET/EF Core) when...
You need maximum performance
You need complex queries
Simple key-value patterns suffice
You need JOINs, aggregations
You're building a custom data layer
You want familiar SQL syntax
Data model is hierarchical (keys with prefixes)
Data model is relational
You want full control over serialization
You want automatic mapping
Most applications benefit from the SQL layer. The Core API is useful for:
- Caching layers
- Session storage
- Simple configuration stores
- Custom high-performance scenarios
Your First SQL Database
Now let's add SQL capabilities. The ADO.NET provider gives you familiar patterns — DbConnection, DbCommand, DbDataReader — with full SQL support.
Package required: OutWit.Database.AdoNet
Creating a Connection
Every SQL operation starts with a connection:
using OutWit.Database.AdoNet;
// Create and open a connection
using var connection = new WitDbConnection("Data Source=myapp.witdb");
connection.Open();
// Now you can execute SQL commands
Console.WriteLine(
Loading...
quot;Connected! State: {connection.State}");
Connection string options:
// File database
"Data Source=myapp.witdb"
// In-memory (for testing)
"Data Source=:memory:"
// With encryption
"Data Source=secure.witdb;Encryption=aes-gcm;Password=MySecret123"
// Read-only mode
"Data Source=myapp.witdb;Mode=ReadOnly"
Tip: Always use using to ensure the connection is properly disposed.
Creating Tables
Use CREATE TABLE to define your schema:
using OutWit.Database.AdoNet;
using var connection = new WitDbConnection("Data Source=tasks.witdb");
connection.Open();
using var cmd = connection.CreateCommand();
// Create a Tasks table
cmd.CommandText = """
CREATE TABLE IF NOT EXISTS Tasks (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Title VARCHAR(200) NOT NULL,
Description TEXT,
Priority INTEGER DEFAULT 0,
IsComplete BOOLEAN DEFAULT FALSE,
DueDate DATETIME,
CreatedAt DATETIME DEFAULT NOW()
)
""";
cmd.ExecuteNonQuery();
Console.WriteLine("Table created successfully!");
Supported data types:
SQL Type
.NET Type
Notes
INTEGER
int, long
64-bit signed integer
REAL
double
64-bit floating point
TEXT
string
UTF-8 string
BLOB
byte[]
Binary data
BOOLEAN
bool
Stored as 0/1
DATETIME
DateTime
ISO 8601 format
DECIMAL(p,s)
decimal
Exact numeric
VARCHAR(n)
string
Limited length string
GUID
Guid
16-byte UUID
Common constraints:
-- Primary key with auto-increment
Id INTEGER PRIMARY KEY AUTOINCREMENT
-- Not null
Title VARCHAR(200) NOT NULL
-- Default value
Priority INTEGER DEFAULT 0
CreatedAt DATETIME DEFAULT NOW()
-- Unique
Email VARCHAR(255) UNIQUE
-- Check constraint
CHECK (Priority >= 0 AND Priority <= 5)
-- Foreign key
FOREIGN KEY (UserId) REFERENCES Users(Id) ON DELETE CASCADE
Inserting Data
Simple Insert
using var cmd = connection.CreateCommand();
cmd.CommandText = "INSERT INTO Tasks (Title, Priority) VALUES ('Buy groceries', 1)";
int rowsAffected = cmd.ExecuteNonQuery();
Console.WriteLine(
Loading...
quot;Inserted {rowsAffected} row(s)");
Parameterized Insert (Recommended)
Always use parameters to prevent SQL injection and handle special characters:
using var cmd = connection.CreateCommand();
cmd.CommandText = """
INSERT INTO Tasks (Title, Description, Priority, DueDate)
VALUES (@title, @description, @priority, @dueDate)
""";
cmd.Parameters.Add(new WitDbParameter("@title", "Finish report"));
cmd.Parameters.Add(new WitDbParameter("@description", "Q4 financial summary"));
cmd.Parameters.Add(new WitDbParameter("@priority", 2));
cmd.Parameters.Add(new WitDbParameter("@dueDate", DateTime.Now.AddDays(7)));
cmd.ExecuteNonQuery();
Getting the Inserted ID
// Method 1: Use RETURNING clause (recommended)
cmd.CommandText = """
INSERT INTO Tasks (Title, Priority)
VALUES (@title, @priority)
RETURNING Id
""";
cmd.Parameters.Add(new WitDbParameter("@title", "New task"));
cmd.Parameters.Add(new WitDbParameter("@priority", 1));
var newId = cmd.ExecuteScalar();
Console.WriteLine(
Loading...
quot;Inserted task with Id: {newId}");
// Method 2: Use LAST_INSERT_ROWID()
cmd.CommandText = "INSERT INTO Tasks (Title) VALUES ('Another task')";
cmd.ExecuteNonQuery();
cmd.CommandText = "SELECT LAST_INSERT_ROWID()";
var lastId = cmd.ExecuteScalar();
Console.WriteLine(
Loading...
quot;Last inserted Id: {lastId}");
Bulk Insert
For inserting many rows efficiently, use a transaction:
using var transaction = connection.BeginTransaction();
using var cmd = connection.CreateCommand();
cmd.Transaction = transaction;
cmd.CommandText = "INSERT INTO Tasks (Title, Priority) VALUES (@title, @priority)";
var titleParam = new WitDbParameter("@title", "");
var priorityParam = new WitDbParameter("@priority", 0);
cmd.Parameters.Add(titleParam);
cmd.Parameters.Add(priorityParam);
// Prepare the command for repeated execution
cmd.Prepare();
// Insert 1000 tasks
for (int i = 1; i <= 1000; i++)
{
titleParam.Value =
Loading...
quot;Task #{i}";
priorityParam.Value = i % 5;
cmd.ExecuteNonQuery();
}
transaction.Commit();
Console.WriteLine("Inserted 1000 tasks");
Querying Data
ExecuteReader — Multiple Rows
using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT Id, Title, Priority, IsComplete FROM Tasks ORDER BY Priority DESC";
using var reader = cmd.ExecuteReader();
Console.WriteLine("Tasks:");
Console.WriteLine("─".PadRight(60, '─'));
while (reader.Read())
{
var id = reader.GetInt32(0); // By index
var title = reader["Title"]; // By name
var priority = reader.GetInt32(2);
var isComplete = reader.GetBoolean(3);
var status = isComplete ? "✓" : "○";
Console.WriteLine(
Loading...
quot; {status} [{priority}] {title} (Id: {id})");
}
ExecuteScalar — Single Value
cmd.CommandText = "SELECT COUNT(*) FROM Tasks WHERE IsComplete = FALSE";
var incompleteCount = Convert.ToInt32(cmd.ExecuteScalar());
Console.WriteLine(
Loading...
quot;Incomplete tasks: {incompleteCount}");
cmd.CommandText = "SELECT MAX(Priority) FROM Tasks";
var maxPriority = cmd.ExecuteScalar();
Console.WriteLine(
Loading...
quot;Highest priority: {maxPriority}");
Filtering with WHERE
cmd.CommandText = """
SELECT Id, Title, DueDate
FROM Tasks
WHERE Priority >= @minPriority
AND IsComplete = FALSE
AND DueDate <= @deadline
ORDER BY DueDate
""";
cmd.Parameters.Clear();
cmd.Parameters.Add(new WitDbParameter("@minPriority", 2));
cmd.Parameters.Add(new WitDbParameter("@deadline", DateTime.Now.AddDays(7)));
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
Console.WriteLine(
Loading...
quot; {reader["Title"]} - Due: {reader["DueDate"]}");
}
Checking for NULL Values
while (reader.Read())
{
var title = reader.GetString(reader.GetOrdinal("Title"));
// Check for NULL before reading
var descriptionOrdinal = reader.GetOrdinal("Description");
var description = reader.IsDBNull(descriptionOrdinal)
? "(no description)"
: reader.GetString(descriptionOrdinal);
Console.WriteLine(
Loading...
quot; {title}: {description}");
}
Mapping to Objects
A common pattern is mapping query results to C# objects:
public record Task(
int Id,
string Title,
string? Description,
int Priority,
bool IsComplete,
DateTime? DueDate,
DateTime CreatedAt
);
public static List<Task> GetAllTasks(WitDbConnection connection)
{
var tasks = new List<Task>();
using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT * FROM Tasks ORDER BY CreatedAt DESC";
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
tasks.Add(new Task(
Id: reader.GetInt32(reader.GetOrdinal("Id")),
Title: reader.GetString(reader.GetOrdinal("Title")),
Description: reader.IsDBNull(reader.GetOrdinal("Description"))
? null
: reader.GetString(reader.GetOrdinal("Description")),
Priority: reader.GetInt32(reader.GetOrdinal("Priority")),
IsComplete: reader.GetBoolean(reader.GetOrdinal("IsComplete")),
DueDate: reader.IsDBNull(reader.GetOrdinal("DueDate"))
? null
: reader.GetDateTime(reader.GetOrdinal("DueDate")),
CreatedAt: reader.GetDateTime(reader.GetOrdinal("CreatedAt"))
));
}
return tasks;
}
// Usage
var allTasks = GetAllTasks(connection);
foreach (var task in allTasks)
{
Console.WriteLine(
Loading...
quot; [{task.Priority}] {task.Title}");
}
Updating Data
Update Specific Rows
using var cmd = connection.CreateCommand();
cmd.CommandText = """
UPDATE Tasks
SET IsComplete = TRUE
WHERE Id = @id
""";
cmd.Parameters.Add(new WitDbParameter("@id", 5));
int rowsAffected = cmd.ExecuteNonQuery();
Console.WriteLine(
Loading...
quot;Updated {rowsAffected} row(s)");
Update with RETURNING
Get the updated values in a single query:
cmd.CommandText = """
UPDATE Tasks
SET Priority = Priority + 1
WHERE Id = @id
RETURNING Id, Title, Priority
""";
cmd.Parameters.Clear();
cmd.Parameters.Add(new WitDbParameter("@id", 3));
using var reader = cmd.ExecuteReader();
if (reader.Read())
{
Console.WriteLine(
Loading...
quot;Updated: {reader["Title"]} - New priority: {reader["Priority"]}");
}
Bulk Update
// Mark all overdue tasks as high priority
cmd.CommandText = """
UPDATE Tasks
SET Priority = 5
WHERE DueDate < @now AND IsComplete = FALSE
""";
cmd.Parameters.Clear();
cmd.Parameters.Add(new WitDbParameter("@now", DateTime.Now));
int updated = cmd.ExecuteNonQuery();
Console.WriteLine(
Loading...
quot;Marked {updated} overdue task(s) as high priority");
Deleting Data
Delete Specific Rows
using var cmd = connection.CreateCommand();
cmd.CommandText = "DELETE FROM Tasks WHERE Id = @id";
cmd.Parameters.Add(new WitDbParameter("@id", 10));
int deleted = cmd.ExecuteNonQuery();
Console.WriteLine(
Loading...
quot;Deleted {deleted} row(s)");
Delete with Condition
// Delete all completed tasks older than 30 days
cmd.CommandText = """
DELETE FROM Tasks
WHERE IsComplete = TRUE
AND CreatedAt < @cutoffDate
""";
cmd.Parameters.Clear();
cmd.Parameters.Add(new WitDbParameter("@cutoffDate", DateTime.Now.AddDays(-30)));
int cleaned = cmd.ExecuteNonQuery();
Console.WriteLine(
Loading...
quot;Cleaned up {cleaned} old completed task(s)");
Delete with RETURNING
cmd.CommandText = """
DELETE FROM Tasks
WHERE Id = @id
RETURNING Id, Title
""";
cmd.Parameters.Clear();
cmd.Parameters.Add(new WitDbParameter("@id", 7));
using var reader = cmd.ExecuteReader();
if (reader.Read())
{
Console.WriteLine(
Loading...
quot;Deleted task: {reader["Title"]}");
}
else
{
Console.WriteLine("Task not found");
}
Checking Changes
Use CHANGES() to see how many rows were affected by the last statement:
cmd.CommandText = "UPDATE Tasks SET Priority = 3 WHERE Priority > 3";
cmd.ExecuteNonQuery();
cmd.CommandText = "SELECT CHANGES()";
var changes = Convert.ToInt32(cmd.ExecuteScalar());
Console.WriteLine(
Loading...
quot;Rows affected: {changes}");
Complete Example: Task Manager
Here's a complete, working example that demonstrates all CRUD operations:
using OutWit.Database.AdoNet;
// ═══════════════════════════════════════════════════════════════════
// Task Manager - Complete ADO.NET Example
// ═══════════════════════════════════════════════════════════════════
using var connection = new WitDbConnection("Data Source=taskmanager.witdb");
connection.Open();
// ───────────────────────────────────────────────────────────────────
// 1. Create the schema
// ───────────────────────────────────────────────────────────────────
ExecuteNonQuery(connection, """
CREATE TABLE IF NOT EXISTS Tasks (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Title VARCHAR(200) NOT NULL,
Description TEXT,
Priority INTEGER DEFAULT 0 CHECK (Priority >= 0 AND Priority <= 5),
IsComplete BOOLEAN DEFAULT FALSE,
DueDate DATETIME,
CreatedAt DATETIME DEFAULT NOW()
)
""");
Console.WriteLine("✓ Schema ready\n");
// ───────────────────────────────────────────────────────────────────
// 2. Insert sample tasks
// ───────────────────────────────────────────────────────────────────
var tasks = new[]
{
("Write documentation", "Complete the Quick Start guide", 3, DateTime.Now.AddDays(2)),
("Review pull requests", null as string, 2, DateTime.Now.AddDays(1)),
("Fix bug #1234", "NullReferenceException in OrderService", 5, DateTime.Now),
("Update dependencies", "Upgrade to .NET 10", 1, DateTime.Now.AddDays(14)),
("Team meeting", "Weekly sync", 2, DateTime.Now.AddDays(3)),
};
foreach (var (title, description, priority, dueDate) in tasks)
{
InsertTask(connection, title, description, priority, dueDate);
}
Console.WriteLine(
Loading...
quot;✓ Inserted {tasks.Length} tasks\n");
// ───────────────────────────────────────────────────────────────────
// 3. List all tasks
// ───────────────────────────────────────────────────────────────────
Console.WriteLine("All Tasks:");
Console.WriteLine("─".PadRight(70, '─'));
PrintTasks(connection, "SELECT * FROM Tasks ORDER BY Priority DESC, DueDate");
// ───────────────────────────────────────────────────────────────────
// 4. Find high-priority tasks
// ───────────────────────────────────────────────────────────────────
Console.WriteLine("\nHigh Priority Tasks (≥3):");
Console.WriteLine("─".PadRight(70, '─'));
PrintTasks(connection, "SELECT * FROM Tasks WHERE Priority >= 3 ORDER BY Priority DESC");
// ───────────────────────────────────────────────────────────────────
// 5. Complete a task
// ───────────────────────────────────────────────────────────────────
var completedTitle = CompleteTask(connection, 3); // Complete task #3
Console.WriteLine(
Loading...
quot;\n✓ Completed: \"{completedTitle}\"\n");
// ───────────────────────────────────────────────────────────────────
// 6. Show statistics
// ───────────────────────────────────────────────────────────────────
ShowStatistics(connection);
// ───────────────────────────────────────────────────────────────────
// 7. Delete completed tasks
// ───────────────────────────────────────────────────────────────────
int deleted = DeleteCompletedTasks(connection);
Console.WriteLine(
Loading...
quot;\n✓ Deleted {deleted} completed task(s)");
// ───────────────────────────────────────────────────────────────────
// 8. Final task list
// ───────────────────────────────────────────────────────────────────
Console.WriteLine("\nRemaining Tasks:");
Console.WriteLine("─".PadRight(70, '─'));
PrintTasks(connection, "SELECT * FROM Tasks ORDER BY DueDate");
// ═══════════════════════════════════════════════════════════════════
// Helper Methods
// ═══════════════════════════════════════════════════════════════════
static void ExecuteNonQuery(WitDbConnection conn, string sql)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
cmd.ExecuteNonQuery();
}
static int InsertTask(WitDbConnection conn, string title, string? description, int priority, DateTime dueDate)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = """
INSERT INTO Tasks (Title, Description, Priority, DueDate)
VALUES (@title, @desc, @priority, @due)
RETURNING Id
""";
cmd.Parameters.Add(new WitDbParameter("@title", title));
cmd.Parameters.Add(new WitDbParameter("@desc", description ?? (object)DBNull.Value));
cmd.Parameters.Add(new WitDbParameter("@priority", priority));
cmd.Parameters.Add(new WitDbParameter("@due", dueDate));
return Convert.ToInt32(cmd.ExecuteScalar());
}
static void PrintTasks(WitDbConnection conn, string sql)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
var id = reader.GetInt32(reader.GetOrdinal("Id"));
var title = reader.GetString(reader.GetOrdinal("Title"));
var priority = reader.GetInt32(reader.GetOrdinal("Priority"));
var isComplete = reader.GetBoolean(reader.GetOrdinal("IsComplete"));
var dueDate = reader.GetDateTime(reader.GetOrdinal("DueDate"));
var status = isComplete ? "✓" : "○";
var priorityStars = new string('★', priority) + new string('☆', 5 - priority);
var dueDays = (dueDate.Date - DateTime.Today).Days;
var dueText = dueDays switch
{
< 0 =>
Loading...
quot;OVERDUE by {-dueDays}d",
0 => "TODAY",
1 => "Tomorrow",
_ =>
Loading...
quot;in {dueDays}d"
};
Console.WriteLine(
Loading...
quot; {status} #{id,-3} [{priorityStars}] {title,-30} ({dueText})");
}
}
static string CompleteTask(WitDbConnection conn, int taskId)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = """
UPDATE Tasks
SET IsComplete = TRUE
WHERE Id = @id
RETURNING Title
""";
cmd.Parameters.Add(new WitDbParameter("@id", taskId));
return cmd.ExecuteScalar()?.ToString() ?? "(not found)";
}
static void ShowStatistics(WitDbConnection conn)
{
using var cmd = conn.CreateCommand();
Console.WriteLine("\nStatistics:");
Console.WriteLine("─".PadRight(30, '─'));
cmd.CommandText = "SELECT COUNT(*) FROM Tasks";
Console.WriteLine(
Loading...
quot; Total tasks: {cmd.ExecuteScalar()}");
cmd.CommandText = "SELECT COUNT(*) FROM Tasks WHERE IsComplete = TRUE";
Console.WriteLine(
Loading...
quot; Completed: {cmd.ExecuteScalar()}");
cmd.CommandText = "SELECT COUNT(*) FROM Tasks WHERE IsComplete = FALSE";
Console.WriteLine(
Loading...
quot; Pending: {cmd.ExecuteScalar()}");
cmd.CommandText = "SELECT COUNT(*) FROM Tasks WHERE DueDate < @now AND IsComplete = FALSE";
cmd.Parameters.Add(new WitDbParameter("@now", DateTime.Now));
Console.WriteLine(
Loading...
quot; Overdue: {cmd.ExecuteScalar()}");
cmd.Parameters.Clear();
cmd.CommandText = "SELECT AVG(Priority) FROM Tasks WHERE IsComplete = FALSE";
var avgPriority = cmd.ExecuteScalar();
Console.WriteLine(
Loading...
quot; Avg priority: {avgPriority:F1}");
}
static int DeleteCompletedTasks(WitDbConnection conn)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "DELETE FROM Tasks WHERE IsComplete = TRUE";
return cmd.ExecuteNonQuery();
}
Sample output:
✓ Schema ready
✓ Inserted 5 tasks
All Tasks:
──────────────────────────────────────────────────────────────────────
○ #3 [★★★★★] Fix bug #1234 (TODAY)
○ #1 [★★★☆☆] Write documentation (in 2d)
○ #2 [★★☆☆☆] Review pull requests (Tomorrow)
○ #5 [★★☆☆☆] Team meeting (in 3d)
○ #4 [★☆☆☆☆] Update dependencies (in 14d)
High Priority Tasks (≥3):
──────────────────────────────────────────────────────────────────────
○ #3 [★★★★★] Fix bug #1234 (TODAY)
○ #1 [★★★☆☆] Write documentation (in 2d)
✓ Completed: "Fix bug #1234"
Statistics:
──────────────────────────────────
Total tasks: 5
Completed: 1
Pending: 4
Overdue: 0
Avg priority: 2.0
✓ Deleted 1 completed task(s)
Remaining Tasks:
──────────────────────────────────────────────────────────────────────
○ #2 [★★☆☆☆] Review pull requests (Tomorrow)
○ #1 [★★★☆☆] Write documentation (in 2d)
○ #5 [★★☆☆☆] Team meeting (in 3d)
○ #4 [★☆☆☆☆] Update dependencies (in 14d)
Async Operations
All ADO.NET operations have async versions:
using var connection = new WitDbConnection("Data Source=app.witdb");
await connection.OpenAsync();
using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT * FROM Tasks";
await using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
// Process row
}
// Or for non-query
cmd.CommandText = "UPDATE Tasks SET Priority = 5 WHERE Id = 1";
await cmd.ExecuteNonQueryAsync();
// Or for scalar
cmd.CommandText = "SELECT COUNT(*) FROM Tasks";
var count = await cmd.ExecuteScalarAsync();
ADO.NET Summary
Operation
Method
Returns
Create/Update/Delete
ExecuteNonQuery()
Rows affected (int)
Single value
ExecuteScalar()
First column of first row (object?)
Multiple rows
ExecuteReader()
WitDbDataReader
Get inserted ID
RETURNING Id or LAST_INSERT_ROWID()
ID value
Best practices:
- Always use parameters — prevents SQL injection, handles escaping
- Use
using — ensures proper disposal of connections, commands, readers
- Use transactions for batch operations — better performance and atomicity
- Use
RETURNING — get affected data in a single round-trip
- Use
Prepare() — for repeated execution of the same command
Your First EF Core Application
Entity Framework Core provides a higher-level abstraction over the database. Instead of writing SQL, you work with C# objects and LINQ queries. EF Core handles the mapping, change tracking, and SQL generation automatically.
Package required: OutWit.Database.EntityFramework
Why EF Core?
ADO.NET
EF Core
Write SQL manually
Write LINQ, SQL generated
Map results to objects manually
Automatic object mapping
Track changes manually
Automatic change tracking
Write migrations manually
Automatic migrations
More control, more code
Less code, more productivity
Use EF Core when:
- You want rapid development
- Your data model maps well to objects
- You need migrations support
- You want to share code with other EF Core providers (SQL Server, PostgreSQL)
Use ADO.NET when:
- You need maximum performance
- You have complex SQL that LINQ can't express
- You want full control over queries
Defining Your Model
Start by defining your entity classes — these represent your database tables:
// A simple Todo item
public class Todo
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string? Description { get; set; }
public bool IsComplete { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? CompletedAt { get; set; }
}
EF Core conventions:
- Property named
Id or <ClassName>Id → Primary Key
string → VARCHAR(MAX) / TEXT
int, long → INTEGER
bool → BOOLEAN
DateTime → DATETIME
- Nullable types (
string?, DateTime?) → NULL allowed
Creating a DbContext
The DbContext is your gateway to the database. It manages entity objects and their relationship to the database:
using Microsoft.EntityFrameworkCore;
using OutWit.Database.EntityFramework;
public class AppDbContext : DbContext
{
// DbSet properties represent tables
public DbSet<Todo> Todos => Set<Todo>();
// Configure the database connection
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
options.UseWitDb("Data Source=todos.witdb");
}
}
With Dependency Injection (ASP.NET Core)
// In Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseWitDb("Data Source=todos.witdb"));
// In your DbContext - remove OnConfiguring, use constructor
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
Creating the Database
using var context = new AppDbContext();
// Create database and tables if they don't exist
context.Database.EnsureCreated();
Console.WriteLine("Database ready!");
Note: EnsureCreated() creates the database based on your model. For production apps, use Migrations instead (covered in Part 4).
Basic CRUD Operations
Create — Adding Entities
using var context = new AppDbContext();
context.Database.EnsureCreated();
// Add a single entity
var todo = new Todo
{
Title = "Learn WitDatabase",
Description = "Complete the Quick Start guide"
};
context.Todos.Add(todo);
context.SaveChanges();
Console.WriteLine(
Loading...
quot;Created todo with Id: {todo.Id}");
Adding multiple entities:
var todos = new[]
{
new Todo { Title = "Buy groceries" },
new Todo { Title = "Call mom" },
new Todo { Title = "Finish report", Description = "Q4 summary" }
};
context.Todos.AddRange(todos);
context.SaveChanges();
Console.WriteLine(
Loading...
quot;Created {todos.Length} todos");
Read — Querying Entities
Get all entities:
var allTodos = context.Todos.ToList();
foreach (var todo in allTodos)
{
Console.WriteLine(
Loading...
quot; [{(todo.IsComplete ? "x" : " ")}] {todo.Title}");
}
Find by primary key:
var todo = context.Todos.Find(1);
if (todo != null)
{
Console.WriteLine(
Loading...
quot;Found: {todo.Title}");
}
Query with conditions:
// Incomplete todos
var incomplete = context.Todos
.Where(t => !t.IsComplete)
.ToList();
// Single item (throws if not found or multiple)
var specific = context.Todos
.Single(t => t.Title == "Learn WitDatabase");
// First matching (or null)
var first = context.Todos
.FirstOrDefault(t => t.Title.Contains("report"));
// Check existence
bool hasIncomplete = context.Todos.Any(t => !t.IsComplete);
// Count
int incompleteCount = context.Todos.Count(t => !t.IsComplete);
Update — Modifying Entities
// Find the entity
var todo = context.Todos.Find(1);
if (todo != null)
{
// Modify properties
todo.IsComplete = true;
todo.CompletedAt = DateTime.UtcNow;
// Save changes
context.SaveChanges();
Console.WriteLine(
Loading...
quot;Completed: {todo.Title}");
}
Bulk update pattern:
// Mark all old incomplete todos as complete
var oldTodos = context.Todos
.Where(t => !t.IsComplete && t.CreatedAt < DateTime.UtcNow.AddDays(-30))
.ToList();
foreach (var todo in oldTodos)
{
todo.IsComplete = true;
todo.CompletedAt = DateTime.UtcNow;
}
context.SaveChanges();
Console.WriteLine(
Loading...
quot;Auto-completed {oldTodos.Count} old todos");
Delete — Removing Entities
// Find and remove
var todo = context.Todos.Find(5);
if (todo != null)
{
context.Todos.Remove(todo);
context.SaveChanges();
Console.WriteLine(
Loading...
quot;Deleted: {todo.Title}");
}
Remove multiple:
// Remove all completed todos
var completed = context.Todos
.Where(t => t.IsComplete)
.ToList();
context.Todos.RemoveRange(completed);
context.SaveChanges();
Console.WriteLine(
Loading...
quot;Removed {completed.Count} completed todos");
LINQ Queries
EF Core translates LINQ to SQL. Here are common patterns:
Filtering and Sorting
var results = context.Todos
.Where(t => !t.IsComplete) // WHERE IsComplete = FALSE
.Where(t => t.Title.Contains("report")) // AND Title LIKE '%report%'
.OrderByDescending(t => t.CreatedAt) // ORDER BY CreatedAt DESC
.ThenBy(t => t.Title) // THEN BY Title
.ToList();
Selecting Specific Columns
// Anonymous type projection
var titles = context.Todos
.Where(t => !t.IsComplete)
.Select(t => new { t.Id, t.Title })
.ToList();
foreach (var item in titles)
{
Console.WriteLine(
Loading...
quot; #{item.Id}: {item.Title}");
}
Pagination
int page = 2;
int pageSize = 10;
var pagedTodos = context.Todos
.OrderBy(t => t.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();
Aggregations
// Count
int total = context.Todos.Count();
int incomplete = context.Todos.Count(t => !t.IsComplete);
// Any / All
bool hasAny = context.Todos.Any();
bool allComplete = context.Todos.All(t => t.IsComplete);
// Min / Max
var oldest = context.Todos.Min(t => t.CreatedAt);
var newest = context.Todos.Max(t => t.CreatedAt);
Grouping
var stats = context.Todos
.GroupBy(t => t.IsComplete)
.Select(g => new
{
IsComplete = g.Key,
Count = g.Count()
})
.ToList();
foreach (var stat in stats)
{
var label = stat.IsComplete ? "Complete" : "Incomplete";
Console.WriteLine(
Loading...
quot; {label}: {stat.Count}");
}
Working with Relationships
Defining Relationships
public class User
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
// Navigation property - one user has many todos
public List<Todo> Todos { get; set; } = new();
}
public class Todo
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public bool IsComplete { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// Foreign key
public int UserId { get; set; }
// Navigation property - each todo belongs to one user
public User User { get; set; } = null!;
}
public class AppDbContext : DbContext
{
public DbSet<User> Users => Set<User>();
public DbSet<Todo> Todos => Set<Todo>();
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseWitDb("Data Source=app.witdb");
}
Creating Related Entities
using var context = new AppDbContext();
context.Database.EnsureCreated();
// Create a user with todos
var user = new User
{
Name = "Alice",
Email = "alice@example.com",
Todos = new List<Todo>
{
new() { Title = "Buy milk" },
new() { Title = "Call dentist" },
new() { Title = "Finish project" }
}
};
context.Users.Add(user);
context.SaveChanges();
Console.WriteLine(
Loading...
quot;Created user {user.Name} with {user.Todos.Count} todos");
Querying with Include
By default, navigation properties are not loaded. Use Include to load them:
// Load user with their todos
var userWithTodos = context.Users
.Include(u => u.Todos)
.FirstOrDefault(u => u.Name == "Alice");
if (userWithTodos != null)
{
Console.WriteLine(
Loading...
quot;User: {userWithTodos.Name}");
foreach (var todo in userWithTodos.Todos)
{
Console.WriteLine(
Loading...
quot; - {todo.Title}");
}
}
Querying Related Data
// Find todos for a specific user
var aliceTodos = context.Todos
.Include(t => t.User)
.Where(t => t.User.Name == "Alice")
.ToList();
// Or using the navigation property
var alice = context.Users
.Include(u => u.Todos)
.First(u => u.Name == "Alice");
foreach (var todo in alice.Todos.Where(t => !t.IsComplete))
{
Console.WriteLine(
Loading...
quot; {todo.Title}");
}
Model Configuration
Using Data Annotations
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
public class Product
{
public int Id { get; set; }
[Required]
[MaxLength(100)]
public string Name { get; set; } = string.Empty;
[MaxLength(500)]
public string? Description { get; set; }
[Column(TypeName = "decimal(18,2)")]
public decimal Price { get; set; }
[Required]
public string SKU { get; set; } = string.Empty;
}
Using Fluent API
public class AppDbContext : DbContext
{
public DbSet<Product> Products => Set<Product>();
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseWitDb("Data Source=shop.witdb");
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>(entity =>
{
// Primary key
entity.HasKey(e => e.Id);
// Required field with max length
entity.Property(e => e.Name)
.IsRequired()
.HasMaxLength(100);
// Optional field
entity.Property(e => e.Description)
.HasMaxLength(500);
// Decimal precision
entity.Property(e => e.Price)
.HasColumnType("decimal(18,2)");
// Unique index
entity.HasIndex(e => e.SKU)
.IsUnique();
// Composite index
entity.HasIndex(e => new { e.Name, e.Price });
});
}
}
Complete Example: Todo Application
Here's a complete, working example with users and todos:
using Microsoft.EntityFrameworkCore;
using OutWit.Database.EntityFramework;
// ═══════════════════════════════════════════════════════════════════
// Todo Application - Complete EF Core Example
// ═══════════════════════════════════════════════════════════════════
// Run the application
await RunTodoApp();
async Task RunTodoApp()
{
await using var context = new TodoDbContext();
await context.Database.EnsureCreatedAsync();
Console.WriteLine("╔══════════════════════════════════════════════════════════════╗");
Console.WriteLine("║ Todo Application with EF Core ║");
Console.WriteLine("╚══════════════════════════════════════════════════════════════╝\n");
// ─────────────────────────────────────────────────────────────────
// 1. Create users
// ─────────────────────────────────────────────────────────────────
if (!await context.Users.AnyAsync())
{
var users = new[]
{
new User { Name = "Alice", Email = "alice@example.com" },
new User { Name = "Bob", Email = "bob@example.com" }
};
context.Users.AddRange(users);
await context.SaveChangesAsync();
Console.WriteLine(
Loading...
quot;✓ Created {users.Length} users\n");
}
// ─────────────────────────────────────────────────────────────────
// 2. Create todos for Alice
// ─────────────────────────────────────────────────────────────────
var alice = await context.Users.FirstAsync(u => u.Name == "Alice");
if (!await context.Todos.AnyAsync(t => t.UserId == alice.Id))
{
var aliceTodos = new[]
{
new Todo { Title = "Review PR #123", Priority = Priority.High, UserId = alice.Id },
new Todo { Title = "Write documentation", Priority = Priority.Medium, UserId = alice.Id },
new Todo { Title = "Update dependencies", Priority = Priority.Low, UserId = alice.Id },
new Todo { Title = "Fix login bug", Priority = Priority.Critical, UserId = alice.Id },
new Todo { Title = "Team meeting", Priority = Priority.Medium, UserId = alice.Id },
};
context.Todos.AddRange(aliceTodos);
await context.SaveChangesAsync();
Console.WriteLine(
Loading...
quot;✓ Created {aliceTodos.Length} todos for Alice\n");
}
// ─────────────────────────────────────────────────────────────────
// 3. List all users with todo counts
// ─────────────────────────────────────────────────────────────────
Console.WriteLine("Users:");
Console.WriteLine("─".PadRight(50, '─'));
var usersWithCounts = await context.Users
.Select(u => new
{
u.Name,
u.Email,
TodoCount = u.Todos.Count,
IncompleteCount = u.Todos.Count(t => !t.IsComplete)
})
.ToListAsync();
foreach (var user in usersWithCounts)
{
Console.WriteLine(
Loading...
quot; {user.Name} ({user.Email})");
Console.WriteLine(
Loading...
quot; Todos: {user.TodoCount} total, {user.IncompleteCount} incomplete\n");
}
// ─────────────────────────────────────────────────────────────────
// 4. List Alice's todos by priority
// ─────────────────────────────────────────────────────────────────
Console.WriteLine("Alice's Todos (by priority):");
Console.WriteLine("─".PadRight(50, '─'));
var aliceTodosList = await context.Todos
.Where(t => t.User.Name == "Alice")
.OrderByDescending(t => t.Priority)
.ToListAsync();
foreach (var todo in aliceTodosList)
{
var status = todo.IsComplete ? "✓" : "○";
var priority = todo.Priority.ToString().PadRight(8);
Console.WriteLine(
Loading...
quot; {status} [{priority}] {todo.Title}");
}
// ─────────────────────────────────────────────────────────────────
// 5. Complete the critical bug
// ─────────────────────────────────────────────────────────────────
var criticalBug = await context.Todos
.FirstOrDefaultAsync(t => t.Title.Contains("bug") && t.Priority == Priority.Critical);
if (criticalBug != null && !criticalBug.IsComplete)
{
criticalBug.IsComplete = true;
criticalBug.CompletedAt = DateTime.UtcNow;
await context.SaveChangesAsync();
Console.WriteLine(
Loading...
quot;\n✓ Completed: {criticalBug.Title}");
}
// ─────────────────────────────────────────────────────────────────
// 6. Show statistics
// ─────────────────────────────────────────────────────────────────
Console.WriteLine("\nStatistics:");
Console.WriteLine("─".PadRight(50, '─'));
var stats = await context.Todos
.GroupBy(t => t.Priority)
.Select(g => new { Priority = g.Key, Count = g.Count(), Completed = g.Count(t => t.IsComplete) })
.OrderByDescending(s => s.Priority)
.ToListAsync();
foreach (var stat in stats)
{
var pct = stat.Count > 0 ? (stat.Completed * 100 / stat.Count) : 0;
Console.WriteLine(
Loading...
quot; {stat.Priority,-10}: {stat.Completed}/{stat.Count} complete ({pct}%)");
}
// ─────────────────────────────────────────────────────────────────
// 7. Find overdue or high-priority incomplete todos
// ─────────────────────────────────────────────────────────────────
Console.WriteLine("\n⚠ Action Required:");
Console.WriteLine("─".PadRight(50, '─'));
var actionRequired = await context.Todos
.Include(t => t.User)
.Where(t => !t.IsComplete && t.Priority >= Priority.High)
.OrderByDescending(t => t.Priority)
.ToListAsync();
if (actionRequired.Any())
{
foreach (var todo in actionRequired)
{
Console.WriteLine(
Loading...
quot; [{todo.Priority}] {todo.Title} (assigned to {todo.User.Name})");
}
}
else
{
Console.WriteLine(" No high-priority items pending!");
}
Console.WriteLine("\n✓ Done!");
}
// ═══════════════════════════════════════════════════════════════════
// Entity Classes
// ═══════════════════════════════════════════════════════════════════
public enum Priority
{
Low = 0,
Medium = 1,
High = 2,
Critical = 3
}
public class User
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public List<Todo> Todos { get; set; } = new();
}
public class Todo
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string? Description { get; set; }
public Priority Priority { get; set; } = Priority.Medium;
public bool IsComplete { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? CompletedAt { get; set; }
public int UserId { get; set; }
public User User { get; set; } = null!;
}
// ═══════════════════════════════════════════════════════════════════
// DbContext
// ═══════════════════════════════════════════════════════════════════
public class TodoDbContext : DbContext
{
public DbSet<User> Users => Set<User>();
public DbSet<Todo> Todos => Set<Todo>();
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
options.UseWitDb("Data Source=todo-app.witdb");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// User configuration
modelBuilder.Entity<User>(entity =>
{
entity.HasIndex(e => e.Email).IsUnique();
entity.Property(e => e.Name).HasMaxLength(100);
entity.Property(e => e.Email).HasMaxLength(255);
});
// Todo configuration
modelBuilder.Entity<Todo>(entity =>
{
entity.Property(e => e.Title).HasMaxLength(200);
entity.Property(e => e.Description).HasMaxLength(1000);
// Relationship
entity.HasOne(t => t.User)
.WithMany(u => u.Todos)
.HasForeignKey(t => t.UserId)
.OnDelete(DeleteBehavior.Cascade);
});
}
}
Sample output:
╔══════════════════════════════════════════════════════════════╗
║ Todo Application with EF Core ║
╚══════════════════════════════════════════════════════════════╝
✓ Created 2 users
✓ Created 5 todos for Alice
Users:
──────────────────────────────────────────────────
Alice (alice@example.com)
Todos: 5 total, 5 incomplete
Bob (bob@example.com)
Todos: 0 total, 0 incomplete
Alice's Todos (by priority):
──────────────────────────────────────────────────
○ [Critical] Fix login bug
○ [High ] Review PR #123
○ [Medium ] Write documentation
○ [Medium ] Team meeting
○ [Low ] Update dependencies
✓ Completed: Fix login bug
Statistics:
──────────────────────────────────────────────────
Critical : 1/1 complete (100%)
High : 0/1 complete (0%)
Medium : 0/2 complete (0%)
Low : 0/1 complete (0%)
⚠ Action Required:
──────────────────────────────────────────────────
[High] Review PR #123 (assigned to Alice)
✓ Done!
Async Best Practices
EF Core is designed for async operations. Use them for better scalability:
// Prefer async methods
var todos = await context.Todos.ToListAsync();
var todo = await context.Todos.FindAsync(id);
var count = await context.Todos.CountAsync();
var exists = await context.Todos.AnyAsync(t => t.Id == id);
var first = await context.Todos.FirstOrDefaultAsync(t => t.Title == "Test");
await context.SaveChangesAsync();
await context.Database.EnsureCreatedAsync();
Async enumeration for large datasets:
await foreach (var todo in context.Todos.AsAsyncEnumerable())
{
// Process one at a time, doesn't load all into memory
Console.WriteLine(todo.Title);
}
EF Core Summary
Task
Code
Create entity
context.Todos.Add(todo); context.SaveChanges();
Read all
context.Todos.ToList()
Find by ID
context.Todos.Find(id)
Query with filter
context.Todos.Where(t => ...).ToList()
Update
todo.Title = "New"; context.SaveChanges();
Delete
context.Todos.Remove(todo); context.SaveChanges();
Include related
context.Todos.Include(t => t.User).ToList()
EF Core advantages with WitDatabase:
- Same patterns as SQL Server, PostgreSQL, MySQL
- Automatic change tracking
- LINQ queries translated to SQL
- Navigation properties and relationships
- Migrations support (see Part 4)
- Easy to switch providers for testing/production
Adding Encryption
WitDatabase provides built-in encryption to protect your data at rest. All data is encrypted before writing to disk and decrypted when reading — completely transparent to your application code.
Why Encrypt?
Scenario
Risk Without Encryption
Desktop application
User's files can be opened by anyone with file access
Mobile app
Device theft exposes all data
Shared hosting
Other tenants might access your files
Backup storage
Backups contain readable data
Compliance (GDPR, HIPAA)
May require encryption at rest
WitDatabase encryption provides:
- AES-256-GCM — industry standard, hardware-accelerated
- ChaCha20-Poly1305 — fast in software, ideal for WASM
- Page-level encryption — each 4KB page encrypted independently
- Authenticated encryption — detects tampering
Quick Start: Password-Based Encryption
The simplest way to enable encryption — just add password to your connection string:
ADO.NET
using OutWit.Database.AdoNet;
// Just add Encryption and Password parameters
var connectionString = "Data Source=secure.witdb;Encryption=aes-gcm;Password=MySecretPassword123!";
using var connection = new WitDbConnection(connectionString);
connection.Open();
// Use exactly like an unencrypted database
using var cmd = connection.CreateCommand();
cmd.CommandText = "CREATE TABLE IF NOT EXISTS Secrets (Id INTEGER PRIMARY KEY, Data TEXT)";
cmd.ExecuteNonQuery();
cmd.CommandText = "INSERT INTO Secrets (Data) VALUES (@data)";
cmd.Parameters.Add(new WitDbParameter("@data", "This is encrypted!"));
cmd.ExecuteNonQuery();
Console.WriteLine("Data saved with AES-256 encryption!");
Entity Framework Core
using Microsoft.EntityFrameworkCore;
using OutWit.Database.EntityFramework;
public class SecureDbContext : DbContext
{
public DbSet<Secret> Secrets => Set<Secret>();
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
options.UseWitDb("Data Source=secure.witdb;Encryption=aes-gcm;Password=MySecretPassword123!");
}
}
public class Secret
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
}
// Usage
using var context = new SecureDbContext();
context.Database.EnsureCreated();
context.Secrets.Add(new Secret { Name = "API_KEY", Value = "sk-1234567890" });
context.SaveChangesAsync();
Core API
using OutWit.Database.Core.Builder;
// Using builder pattern
using var db = new WitDatabaseBuilder()
.WithFilePath("secure.witdb")
.WithBTree()
.WithEncryption("MySecretPassword123!")
.Build();
db.Put("secret-key"u8.ToArray(), "secret-value"u8.ToArray());
Encryption Algorithms
WitDatabase supports two encryption algorithms:
AES-256-GCM (Default)
Best for: Desktop, server, mobile (with AES-NI hardware support)
// Connection string
"Data Source=app.witdb;Encryption=aes-gcm;Password=secret"
// Builder
.WithEncryption("password") // AES-GCM is default
Characteristics:
- Hardware-accelerated on modern CPUs (AES-NI instruction set)
- 256-bit key, 128-bit authentication tag
- Industry standard, NIST approved
- ~3-5 GB/s on modern hardware with AES-NI
ChaCha20-Poly1305
Best for: Blazor WebAssembly, ARM devices without AES-NI
// Requires: dotnet add package OutWit.Database.Core.BouncyCastle
// Connection string
"Data Source=app.witdb;Encryption=chacha20;Password=secret"
// Builder
using OutWit.Database.Core.BouncyCastle;
var db = new WitDatabaseBuilder()
.WithFilePath("secure.witdb")
.WithBTree()
.WithBouncyCastleEncryption("password")
.Build();
Characteristics:
- Pure software implementation (no hardware requirements)
- 256-bit key, 128-bit authentication tag
- Faster than AES in software (no AES-NI)
- Designed by Daniel J. Bernstein
- Used by TLS 1.3, WireGuard, Signal
Which to Choose?
Environment
Recommended
Why
Windows/Linux/macOS (x64)
AES-GCM
Hardware acceleration
Blazor WebAssembly
ChaCha20
No AES-NI in browser
ARM without AES extensions
ChaCha20
Faster in software
iOS/Android (modern)
AES-GCM
Most have hardware AES
Maximum compatibility
ChaCha20
Works everywhere
Password Best Practices
Strong Passwords
// ❌ Bad - too short, predictable
"password"
"123456"
"myapp"
// ✅ Good - long, mixed characters
"Hk9$mP2!xL7@nQ4&"
"correct-horse-battery-staple"
"MyApp-2024-SecureDB-#1"
Requirements for strong passwords:
- At least 12 characters (16+ recommended)
- Mix of uppercase, lowercase, numbers, symbols
- Not a dictionary word
- Unique per database
User-Provided Passwords
For user-facing encryption (e.g., password-protected documents):
public class SecureDocumentService
{
public void CreateDocument(string filePath, string userPassword)
{
// Validate password strength
if (userPassword.Length < 8)
throw new ArgumentException("Password must be at least 8 characters");
using var db = new WitDatabaseBuilder()
.WithFilePath(filePath)
.WithBTree()
.WithEncryption(userPassword)
.Build();
// Initialize document structure...
}
public WitDatabase OpenDocument(string filePath, string userPassword)
{
try
{
var db = new WitDatabaseBuilder()
.WithFilePath(filePath)
.WithBTree()
.WithEncryption(userPassword)
.Build();
// Verify we can read (will fail with wrong password)
db.Get("_meta"u8.ToArray());
return db;
}
catch (CryptographicException)
{
throw new UnauthorizedAccessException("Invalid password");
}
}
}
Application Secrets
For application-level encryption, don't hardcode passwords:
// ❌ Bad - hardcoded password
var conn = new WitDbConnection("Data Source=app.witdb;Encryption=aes-gcm;Password=hardcoded123");
// ✅ Good - from environment variable
var password = Environment.GetEnvironmentVariable("MYAPP_DB_PASSWORD")
?? throw new InvalidOperationException("Database password not configured");
var conn = new WitDbConnection(
Loading...
quot;Data Source=app.witdb;Encryption=aes-gcm;Password={password}");
// ✅ Good - from configuration
var password = Configuration["Database:Password"];
var conn = new WitDbConnection(
Loading...
quot;Data Source=app.witdb;Encryption=aes-gcm;Password={password}");
// ✅ Good - from secure storage (Windows DPAPI, macOS Keychain, etc.)
var password = SecureStorage.GetPassword("myapp-db");
User-Based Key Derivation
For multi-user scenarios, derive unique keys per user:
// Each user gets a unique encryption key derived from username + password
var connectionString =
Loading...
quot;Data Source=user_{userId}.witdb;Encryption=aes-gcm;User={username};Password={password}";
using var connection = new WitDbConnection(connectionString);
connection.Open();
How it works:
- Salt is derived from username (deterministic)
- Key is derived using PBKDF2 with user's password and salt
- Same username + password always produces same key
- Different users can't decrypt each other's data
// Builder API equivalent
var db = new WitDatabaseBuilder()
.WithFilePath(
Loading...
quot;user_{userId}.witdb")
.WithBTree()
.WithEncryption(username, password) // User + password
.Build();
Fast Encryption for WASM
Browser environments (Blazor WebAssembly) are slower. Use optimized key derivation:
// Standard - 100,000 PBKDF2 iterations (secure but slow in WASM)
"Data Source=app.witdb;Encryption=aes-gcm;Password=secret"
// Fast - 10,000 iterations (faster, still secure for most use cases)
"Data Source=app.witdb;Encryption=aes-gcm;Password=secret;Fast Encryption=true"
// Builder API
var db = new WitDatabaseBuilder()
.WithFilePath("app.witdb")
.WithBTree()
.WithEncryptionFast("password") // Optimized for WASM
.Build();
Note: Reduced iterations are still secure against casual attacks. For high-security scenarios in WASM, consider using ChaCha20 which is faster overall.
Encryption with Raw Keys
For advanced scenarios, you can provide the encryption key directly:
// Generate a 256-bit key (32 bytes)
var key = new byte[32];
RandomNumberGenerator.Fill(key);
// Store this key securely (e.g., Azure Key Vault, AWS KMS)
Console.WriteLine(
Loading...
quot;Key (base64): {Convert.ToBase64String(key)}");
// Use the key
var db = new WitDatabaseBuilder()
.WithFilePath("secure.witdb")
.WithBTree()
.WithAesEncryption(key)
.Build();
Use cases:
- Integration with key management services (Azure Key Vault, AWS KMS, HashiCorp Vault)
- Hardware security modules (HSM)
- Key rotation scenarios
Complete Example: Encrypted Password Manager
using System.Security.Cryptography;
using Microsoft.EntityFrameworkCore;
using OutWit.Database.EntityFramework;
// ═══════════════════════════════════════════════════════════════════
// Simple Password Manager with Encrypted Storage
// ═══════════════════════════════════════════════════════════════════
Console.WriteLine("╔══════════════════════════════════════════════════════════════╗");
Console.WriteLine("║ Encrypted Password Manager ║");
Console.WriteLine("╚══════════════════════════════════════════════════════════════╝\n");
// Get master password from user
Console.Write("Enter master password: ");
var masterPassword = Console.ReadLine() ?? throw new InvalidOperationException("Password required");
// Validate password strength
if (masterPassword.Length < 8)
{
Console.WriteLine("Error: Password must be at least 8 characters");
return;
}
try
{
await using var context = new PasswordVaultContext(masterPassword);
await context.Database.EnsureCreatedAsync();
Console.WriteLine("✓ Vault unlocked successfully!\n");
// Add some passwords
if (!await context.Credentials.AnyAsync())
{
var credentials = new[]
{
new Credential
{
Site = "github.com",
Username = "myuser",
Password = GeneratePassword(16),
Notes = "Main development account"
},
new Credential
{
Site = "aws.amazon.com",
Username = "admin@company.com",
Password = GeneratePassword(20),
Notes = "Production AWS account"
},
new Credential
{
Site = "database.example.com",
Username = "dbadmin",
Password = GeneratePassword(24),
Notes = "PostgreSQL admin"
}
};
context.Credentials.AddRange(credentials);
await context.SaveChangesAsync();
Console.WriteLine(
Loading...
quot;✓ Added {credentials.Length} sample credentials\n");
}
// List all credentials
Console.WriteLine("Stored Credentials:");
Console.WriteLine("─".PadRight(70, '─'));
var allCredentials = await context.Credentials
.OrderBy(c => c.Site)
.ToListAsync();
foreach (var cred in allCredentials)
{
Console.WriteLine(
Loading...
quot; Site: {cred.Site}");
Console.WriteLine(
Loading...
quot; Username: {cred.Username}");
Console.WriteLine(
Loading...
quot; Password: {MaskPassword(cred.Password)}");
if (!string.IsNullOrEmpty(cred.Notes))
Console.WriteLine(
Loading...
quot; Notes: {cred.Notes}");
Console.WriteLine();
}
// Show a specific password
Console.Write("Show password for site (or press Enter to skip): ");
var searchSite = Console.ReadLine();
if (!string.IsNullOrWhiteSpace(searchSite))
{
var found = await context.Credentials
.FirstOrDefaultAsync(c => c.Site.Contains(searchSite, StringComparison.OrdinalIgnoreCase));
if (found != null)
{
Console.WriteLine(
Loading...
quot;\n✓ Password for {found.Site}: {found.Password}");
}
else
{
Console.WriteLine(
Loading...
quot;\n✗ No credential found for '{searchSite}'");
}
}
Console.WriteLine("\n✓ Vault locked. Data remains encrypted on disk.");
}
catch (Exception ex) when (ex.Message.Contains("decrypt") || ex.Message.Contains("authentication"))
{
Console.WriteLine("\n✗ Error: Invalid master password or corrupted vault.");
}
// ═══════════════════════════════════════════════════════════════════
// Helper Methods
// ═══════════════════════════════════════════════════════════════════
static string GeneratePassword(int length)
{
const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
var password = new char[length];
var random = RandomNumberGenerator.Create();
var bytes = new byte[length];
random.GetBytes(bytes);
for (int i = 0; i < length; i++)
password[i] = chars[bytes[i] % chars.Length];
return new string(password);
}
static string MaskPassword(string password)
{
if (password.Length <= 4)
return new string('*', password.Length);
return password[..2] + new string('*', password.Length - 4) + password[^2..];
}
// ═══════════════════════════════════════════════════════════════════
// Entity Classes
// ═══════════════════════════════════════════════════════════════════
public class Credential
{
public int Id { get; set; }
public string Site { get; set; } = string.Empty;
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string? Notes { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? ModifiedAt { get; set; }
}
// ═══════════════════════════════════════════════════════════════════
// DbContext with Encryption
// ═══════════════════════════════════════════════════════════════════
public class PasswordVaultContext : DbContext
{
private readonly string _masterPassword;
public PasswordVaultContext(string masterPassword)
{
_masterPassword = masterPassword;
}
public DbSet<Credential> Credentials => Set<Credential>();
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
// AES-256-GCM encryption with master password
options.UseWitDb(
Loading...
quot;Data Source=vault.witdb;Encryption=aes-gcm;Password={_masterPassword}");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Credential>(entity =>
{
entity.HasIndex(e => e.Site);
entity.Property(e => e.Site).HasMaxLength(255);
entity.Property(e => e.Username).HasMaxLength(255);
entity.Property(e => e.Password).HasMaxLength(500);
entity.Property(e => e.Notes).HasMaxLength(2000);
});
}
}
Sample output:
╔══════════════════════════════════════════════════════════════╗
║ Encrypted Password Manager ║
╚══════════════════════════════════════════════════════════════╝
Enter master password: MySecureVault123!
✓ Vault unlocked successfully!
✓ Added 3 sample credentials
Stored Credentials:
──────────────────────────────────────────────────────────────────────
Site: aws.amazon.com
Username: admin@company.com
Password: Kx**************9#
Notes: Production AWS account
Site: database.example.com
Username: dbadmin
Password: 7Q**********************&m
Notes: PostgreSQL admin
Site: github.com
Username: myuser
Password: Np************@3
Notes: Main development account
Show password for site (or press Enter to skip): github
✓ Password for github.com: NpK$8mL2xQ#4nJ@3
✓ Vault locked. Data remains encrypted on disk.
Verifying Encryption
To verify your database is actually encrypted, try opening it without the password:
// Create encrypted database
var db1 = new WitDatabaseBuilder()
.WithFilePath("encrypted.witdb")
.WithBTree()
.WithEncryption("correct-password")
.Build();
db1.Put("key"u8.ToArray(), "secret value"u8.ToArray());
db1.Dispose();
// Try to open without password - should fail
try
{
var db2 = new WitDatabaseBuilder()
.WithFilePath("encrypted.witdb")
.WithBTree()
// No encryption!
.Build();
var value = db2.Get("key"u8.ToArray());
Console.WriteLine("ERROR: Database opened without password!");
}
catch (Exception ex)
{
Console.WriteLine(
Loading...
quot;✓ Correctly failed: {ex.Message}");
}
// Try with wrong password - should fail
try
{
var db3 = new WitDatabaseBuilder()
.WithFilePath("encrypted.witdb")
.WithBTree()
.WithEncryption("wrong-password")
.Build();
var value = db3.Get("key"u8.ToArray());
Console.WriteLine("ERROR: Database opened with wrong password!");
}
catch (Exception ex)
{
Console.WriteLine(
Loading...
quot;✓ Correctly failed: {ex.Message}");
}
Encryption Summary
Scenario
Configuration
Basic encryption
Encryption=aes-gcm;Password=secret
Blazor WASM
Encryption=chacha20;Password=secret
Fast (reduced iterations)
Encryption=aes-gcm;Password=secret;Fast Encryption=true
Per-user encryption
Encryption=aes-gcm;User=username;Password=userpass
Raw key (256-bit)
.WithAesEncryption(keyBytes)
Security checklist:
- ✅ Use strong passwords (12+ characters)
- ✅ Don't hardcode passwords in source code
- ✅ Use environment variables or secure configuration
- ✅ Use ChaCha20 for Blazor WebAssembly
- ✅ Consider per-user key derivation for multi-user apps
Using Transactions
Transactions ensure that a group of operations either all succeed or all fail together. This is essential for maintaining data integrity — you never end up with partially completed operations.
Why Transactions?
Consider transferring money between accounts:
// ❌ Without transaction - DANGEROUS!
account1.Balance -= 100; // Debit
SaveChanges();
// What if the app crashes here?
account2.Balance += 100; // Credit
SaveChanges();
If the application crashes after the debit but before the credit, $100 vanishes! With transactions:
// ✅ With transaction - SAFE
BeginTransaction();
account1.Balance -= 100; // Debit
account2.Balance += 100; // Credit
Commit(); // Both happen, or neither happens
ACID properties guaranteed by transactions:
Property
Meaning
Atomicity
All operations succeed or all fail
Consistency
Database moves from one valid state to another
Isolation
Concurrent transactions don't interfere
Durability
Committed data survives crashes
ADO.NET Transactions
Basic Transaction
using OutWit.Database.AdoNet;
using var connection = new WitDbConnection("Data Source=bank.witdb");
connection.Open();
// Create accounts table
using var setupCmd = connection.CreateCommand();
setupCmd.CommandText = """
CREATE TABLE IF NOT EXISTS Accounts (
Id INTEGER PRIMARY KEY,
Name VARCHAR(100) NOT NULL,
Balance DECIMAL(18,2) NOT NULL DEFAULT 0
)
""";
setupCmd.ExecuteNonQuery();
// Begin transaction
using var transaction = connection.BeginTransaction();
try
{
using var cmd = connection.CreateCommand();
cmd.Transaction = transaction; // Associate command with transaction
// Debit from account 1
cmd.CommandText = "UPDATE Accounts SET Balance = Balance - @amount WHERE Id = @id";
cmd.Parameters.Add(new WitDbParameter("@amount", 100.00m));
cmd.Parameters.Add(new WitDbParameter("@id", 1));
cmd.ExecuteNonQuery();
// Credit to account 2
cmd.Parameters.Clear();
cmd.CommandText = "UPDATE Accounts SET Balance = Balance + @amount WHERE Id = @id";
cmd.Parameters.Add(new WitDbParameter("@amount", 100.00m));
cmd.Parameters.Add(new WitDbParameter("@id", 2));
cmd.ExecuteNonQuery();
// All succeeded - commit
transaction.Commit();
Console.WriteLine("Transfer complete!");
}
catch (Exception ex)
{
// Something failed - rollback all changes
transaction.Rollback();
Console.WriteLine(
Loading...
quot;Transfer failed: {ex.Message}");
}
Transaction with Validation
Check business rules before committing:
using var transaction = connection.BeginTransaction();
try
{
using var cmd = connection.CreateCommand();
cmd.Transaction = transaction;
// Check source account has sufficient funds
cmd.CommandText = "SELECT Balance FROM Accounts WHERE Id = @id";
cmd.Parameters.Add(new WitDbParameter("@id", sourceAccountId));
var balance = Convert.ToDecimal(cmd.ExecuteScalar());
if (balance < transferAmount)
{
throw new InvalidOperationException("Insufficient funds");
}
// Perform transfer
cmd.CommandText = "UPDATE Accounts SET Balance = Balance - @amount WHERE Id = @id";
cmd.Parameters.Clear();
cmd.Parameters.Add(new WitDbParameter("@amount", transferAmount));
cmd.Parameters.Add(new WitDbParameter("@id", sourceAccountId));
cmd.ExecuteNonQuery();
cmd.CommandText = "UPDATE Accounts SET Balance = Balance + @amount WHERE Id = @id";
cmd.Parameters.Clear();
cmd.Parameters.Add(new WitDbParameter("@amount", transferAmount));
cmd.Parameters.Add(new WitDbParameter("@id", targetAccountId));
cmd.ExecuteNonQuery();
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
Bulk Operations with Transaction
Transactions dramatically improve performance for bulk operations:
using var connection = new WitDbConnection("Data Source=bulk.witdb");
connection.Open();
// Without transaction: ~1000 disk writes
// With transaction: 1 disk write at commit
using var transaction = connection.BeginTransaction();
using var cmd = connection.CreateCommand();
cmd.Transaction = transaction;
cmd.CommandText = "INSERT INTO Items (Name, Value) VALUES (@name, @value)";
var nameParam = new WitDbParameter("@name", "");
var valueParam = new WitDbParameter("@value", 0);
cmd.Parameters.Add(nameParam);
cmd.Parameters.Add(valueParam);
cmd.Prepare();
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < 10000; i++)
{
nameParam.Value =
Loading...
quot;Item {i}";
valueParam.Value = i * 10;
cmd.ExecuteNonQuery();
}
transaction.Commit();
stopwatch.Stop();
Console.WriteLine(
Loading...
quot;Inserted 10,000 rows in {stopwatch.ElapsedMilliseconds}ms");
// Typically 50-100ms with transaction vs 5-10 seconds without
Entity Framework Core Transactions
Implicit Transactions
EF Core automatically wraps SaveChanges() in a transaction:
using var context = new AppDbContext();
// All changes are in a single implicit transaction
context.Accounts.Add(new Account { Name = "Alice", Balance = 1000 });
context.Accounts.Add(new Account { Name = "Bob", Balance = 500 });
// Both inserts succeed or both fail
await context.SaveChangesAsync();
Explicit Transactions
For multiple SaveChanges() calls or complex logic:
using var context = new BankDbContext();
using var transaction = await context.Database.BeginTransactionAsync();
try
{
// Find accounts
var source = await context.Accounts.FindAsync(sourceId);
var target = await context.Accounts.FindAsync(targetId);
if (source == null || target == null)
throw new InvalidOperationException("Account not found");
if (source.Balance < amount)
throw new InvalidOperationException("Insufficient funds");
// Perform transfer
source.Balance -= amount;
target.Balance += amount;
// Log the transaction
context.TransactionLogs.Add(new TransactionLog
{
FromAccountId = sourceId,
ToAccountId = targetId,
Amount = amount,
Timestamp = DateTime.UtcNow
});
await context.SaveChangesAsync();
// Commit both the transfer and the log
await transaction.CommitAsync();
Console.WriteLine(
Loading...
quot;Transferred {amount:C} from {source.Name} to {target.Name}");
}
catch
{
await transaction.RollbackAsync();
throw;
}
ExecutionStrategy for Retries
Handle transient failures with automatic retries:
var strategy = context.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
using var transaction = await context.Database.BeginTransactionAsync();
try
{
// Your operations here
source.Balance -= amount;
target.Balance += amount;
await context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
});
Core API Transactions
Basic Transaction
using OutWit.Database.Core.Builder;
using var db = WitDatabase.CreateOrOpen("app.witdb");
using var tx = db.BeginTransaction();
try
{
tx.Put("key1"u8.ToArray(), "value1"u8.ToArray());
tx.Put("key2"u8.ToArray(), "value2"u8.ToArray());
tx.Delete("old-key"u8.ToArray());
tx.Commit();
}
catch
{
tx.Rollback();
throw;
}
Async Transaction
await using var tx = await db.BeginTransactionAsync();
try
{
await tx.PutAsync(key1, value1);
await tx.PutAsync(key2, value2);
await tx.CommitAsync();
}
catch
{
await tx.RollbackAsync();
throw;
}
Savepoints
Savepoints allow partial rollbacks within a transaction. Useful for complex operations where you want to undo part of the work without losing everything.
ADO.NET Savepoints
using var connection = new WitDbConnection("Data Source=app.witdb");
connection.Open();
using var transaction = connection.BeginTransaction();
try
{
using var cmd = connection.CreateCommand();
cmd.Transaction = transaction;
// Step 1: Create order
cmd.CommandText = "INSERT INTO Orders (CustomerId, Total) VALUES (@custId, @total) RETURNING Id";
cmd.Parameters.Add(new WitDbParameter("@custId", customerId));
cmd.Parameters.Add(new WitDbParameter("@total", 0));
var orderId = Convert.ToInt32(cmd.ExecuteScalar());
Console.WriteLine(
Loading...
quot;Created order {orderId}");
// Create savepoint after order creation
transaction.Save("after_order");
decimal orderTotal = 0;
foreach (var item in cartItems)
{
// Create savepoint before each item
transaction.Save("before_item");
try
{
// Check inventory
cmd.CommandText = "SELECT Stock FROM Products WHERE Id = @productId";
cmd.Parameters.Clear();
cmd.Parameters.Add(new WitDbParameter("@productId", item.ProductId));
var stock = Convert.ToInt32(cmd.ExecuteScalar());
if (stock < item.Quantity)
{
Console.WriteLine(
Loading...
quot; Skipping {item.ProductName}: insufficient stock");
transaction.Rollback("before_item"); // Rollback just this item
continue;
}
// Add order item
cmd.CommandText = """
INSERT INTO OrderItems (OrderId, ProductId, Quantity, Price)
VALUES (@orderId, @productId, @qty, @price)
""";
cmd.Parameters.Clear();
cmd.Parameters.Add(new WitDbParameter("@orderId", orderId));
cmd.Parameters.Add(new WitDbParameter("@productId", item.ProductId));
cmd.Parameters.Add(new WitDbParameter("@qty", item.Quantity));
cmd.Parameters.Add(new WitDbParameter("@price", item.Price));
cmd.ExecuteNonQuery();
// Update inventory
cmd.CommandText = "UPDATE Products SET Stock = Stock - @qty WHERE Id = @productId";
cmd.Parameters.Clear();
cmd.Parameters.Add(new WitDbParameter("@qty", item.Quantity));
cmd.Parameters.Add(new WitDbParameter("@productId", item.ProductId));
cmd.ExecuteNonQuery();
orderTotal += item.Price * item.Quantity;
Console.WriteLine(
Loading...
quot; Added {item.ProductName} x{item.Quantity}");
}
catch (Exception ex)
{
Console.WriteLine(
Loading...
quot; Error with {item.ProductName}: {ex.Message}");
transaction.Rollback("before_item");
}
}
if (orderTotal == 0)
{
// No items added - rollback entire order
transaction.Rollback("after_order");
Console.WriteLine("Order cancelled - no items available");
}
else
{
// Update order total
cmd.CommandText = "UPDATE Orders SET Total = @total WHERE Id = @orderId";
cmd.Parameters.Clear();
cmd.Parameters.Add(new WitDbParameter("@total", orderTotal));
cmd.Parameters.Add(new WitDbParameter("@orderId", orderId));
cmd.ExecuteNonQuery();
transaction.Commit();
Console.WriteLine(
Loading...
quot;Order {orderId} completed: {orderTotal:C}");
}
}
catch
{
transaction.Rollback();
throw;
}
EF Core Savepoints
using var transaction = await context.Database.BeginTransactionAsync();
try
{
// Create order
var order = new Order { CustomerId = customerId };
context.Orders.Add(order);
await context.SaveChangesAsync();
// Savepoint after order creation
await transaction.CreateSavepointAsync("after_order");
foreach (var item in cartItems)
{
await transaction.CreateSavepointAsync("before_item");
try
{
var product = await context.Products.FindAsync(item.ProductId);
if (product == null || product.Stock < item.Quantity)
{
await transaction.RollbackToSavepointAsync("before_item");
continue;
}
order.Items.Add(new OrderItem
{
ProductId = item.ProductId,
Quantity = item.Quantity,
Price = item.Price
});
product.Stock -= item.Quantity;
await context.SaveChangesAsync();
}
catch
{
await transaction.RollbackToSavepointAsync("before_item");
}
}
if (!order.Items.Any())
{
await transaction.RollbackToSavepointAsync("after_order");
throw new InvalidOperationException("No items could be added");
}
order.Total = order.Items.Sum(i => i.Price * i.Quantity);
await context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
Isolation Levels
Isolation levels control how transactions interact with each other. Higher isolation prevents more anomalies but may reduce concurrency.
Available Isolation Levels
Level
Dirty Reads
Non-Repeatable Reads
Phantom Reads
Use Case
ReadUncommitted
Yes
Yes
Yes
Maximum speed, stale data OK
ReadCommitted
No
Yes
Yes
Default, good balance
RepeatableRead
No
No
Yes
Same data on re-read
Snapshot
No
No
No
Point-in-time consistency
Serializable
No
No
No
Maximum isolation
Setting Isolation Level (ADO.NET)
using System.Data;
// Specify isolation level when beginning transaction
using var transaction = connection.BeginTransaction(IsolationLevel.Snapshot);
using var cmd = connection.CreateCommand();
cmd.Transaction = transaction;
// All reads see a consistent snapshot of data
cmd.CommandText = "SELECT * FROM Accounts";
// ... reads see data as of transaction start
transaction.Commit();
Setting Isolation Level (EF Core)
using var transaction = await context.Database
.BeginTransactionAsync(IsolationLevel.Snapshot);
// All queries in this transaction see consistent snapshot
var accounts = await context.Accounts.ToListAsync();
await transaction.CommitAsync();
Setting Default Isolation Level
// Connection string
"Data Source=app.witdb;Isolation Level=Snapshot"
// Builder
var db = new WitDatabaseBuilder()
.WithFilePath("app.witdb")
.WithBTree()
.WithTransactions()
.WithDefaultIsolationLevel(IsolationLevel.Snapshot)
.Build();
MVCC (Multi-Version Concurrency Control)
WitDatabase uses MVCC to allow readers and writers to work simultaneously without blocking each other.
How MVCC Works
Time →
Writer Transaction: Reader Transaction:
───────────────────────────────── ─────────────────────────────────
BEGIN
UPDATE Account SET Balance=900 BEGIN
SELECT Balance FROM Account
→ Returns 1000 (old version)
COMMIT
SELECT Balance FROM Account
→ Still returns 1000 (snapshot)
COMMIT
BEGIN (new transaction)
SELECT Balance FROM Account
→ Returns 900 (committed value)
Benefits:
- Readers don't block writers
- Writers don't block readers
- Consistent reads throughout a transaction
- No dirty reads
Enabling MVCC
// Connection string (enabled by default)
"Data Source=app.witdb;MVCC=true"
// Builder
var db = new WitDatabaseBuilder()
.WithFilePath("app.witdb")
.WithBTree()
.WithTransactions()
.WithMvcc()
.Build();
Complete Example: Bank Transfer System
using System.Data;
using OutWit.Database.AdoNet;
// ═══════════════════════════════════════════════════════════════════
// Bank Transfer System - Transaction Example
// ═══════════════════════════════════════════════════════════════════
using var connection = new WitDbConnection("Data Source=bank.witdb");
connection.Open();
// Setup database
InitializeDatabase(connection);
Console.WriteLine("╔══════════════════════════════════════════════════════════════╗");
Console.WriteLine("║ Bank Transfer System ║");
Console.WriteLine("╚══════════════════════════════════════════════════════════════╝\n");
// Show initial balances
ShowBalances(connection);
// Perform some transfers
Console.WriteLine("\nPerforming transfers:");
Console.WriteLine("─".PadRight(50, '─'));
Transfer(connection, "Alice", "Bob", 200);
Transfer(connection, "Bob", "Charlie", 50);
Transfer(connection, "Charlie", "Alice", 75);
// This should fail - insufficient funds
Transfer(connection, "Alice", "Bob", 10000);
// Show final balances
Console.WriteLine("\nFinal balances:");
ShowBalances(connection);
// Show transaction history
ShowHistory(connection);
// ═══════════════════════════════════════════════════════════════════
// Helper Methods
// ═══════════════════════════════════════════════════════════════════
static void InitializeDatabase(WitDbConnection conn)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = """
CREATE TABLE IF NOT EXISTS Accounts (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Name VARCHAR(100) NOT NULL UNIQUE,
Balance DECIMAL(18,2) NOT NULL DEFAULT 0 CHECK (Balance >= 0)
)
""";
cmd.ExecuteNonQuery();
cmd.CommandText = """
CREATE TABLE IF NOT EXISTS Transactions (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
FromAccount VARCHAR(100),
ToAccount VARCHAR(100),
Amount DECIMAL(18,2) NOT NULL,
Status VARCHAR(20) NOT NULL,
Timestamp DATETIME DEFAULT NOW(),
ErrorMessage TEXT
)
""";
cmd.ExecuteNonQuery();
// Seed accounts if empty
cmd.CommandText = "SELECT COUNT(*) FROM Accounts";
if (Convert.ToInt32(cmd.ExecuteScalar()) == 0)
{
cmd.CommandText = """
INSERT INTO Accounts (Name, Balance) VALUES
('Alice', 1000.00),
('Bob', 500.00),
('Charlie', 250.00)
""";
cmd.ExecuteNonQuery();
}
}
static void Transfer(WitDbConnection conn, string fromName, string toName, decimal amount)
{
using var transaction = conn.BeginTransaction(IsolationLevel.Serializable);
try
{
using var cmd = conn.CreateCommand();
cmd.Transaction = transaction;
// Get source account
cmd.CommandText = "SELECT Id, Balance FROM Accounts WHERE Name = @name";
cmd.Parameters.Add(new WitDbParameter("@name", fromName));
int fromId;
decimal fromBalance;
using (var reader = cmd.ExecuteReader())
{
if (!reader.Read())
throw new InvalidOperationException(
Loading...
quot;Account '{fromName}' not found");
fromId = reader.GetInt32(0);
fromBalance = reader.GetDecimal(1);
}
// Get target account
cmd.Parameters.Clear();
cmd.Parameters.Add(new WitDbParameter("@name", toName));
int toId;
using (var reader = cmd.ExecuteReader())
{
if (!reader.Read())
throw new InvalidOperationException(
Loading...
quot;Account '{toName}' not found");
toId = reader.GetInt32(0);
}
// Check sufficient funds
if (fromBalance < amount)
{
throw new InvalidOperationException(
Loading...
quot;Insufficient funds: {fromName} has {fromBalance:C}, needs {amount:C}");
}
// Debit source
cmd.CommandText = "UPDATE Accounts SET Balance = Balance - @amount WHERE Id = @id";
cmd.Parameters.Clear();
cmd.Parameters.Add(new WitDbParameter("@amount", amount));
cmd.Parameters.Add(new WitDbParameter("@id", fromId));
cmd.ExecuteNonQuery();
// Credit target
cmd.CommandText = "UPDATE Accounts SET Balance = Balance + @amount WHERE Id = @id";
cmd.Parameters.Clear();
cmd.Parameters.Add(new WitDbParameter("@amount", amount));
cmd.Parameters.Add(new WitDbParameter("@id", toId));
cmd.ExecuteNonQuery();
// Log successful transaction
LogTransaction(cmd, fromName, toName, amount, "SUCCESS", null);
transaction.Commit();
Console.WriteLine(
Loading...
quot; ✓ {fromName} → {toName}: {amount:C}");
}
catch (Exception ex)
{
transaction.Rollback();
// Log failed transaction (in a new transaction)
using var logCmd = conn.CreateCommand();
LogTransaction(logCmd, fromName, toName, amount, "FAILED", ex.Message);
Console.WriteLine(
Loading...
quot; ✗ {fromName} → {toName}: {amount:C} - {ex.Message}");
}
}
static void LogTransaction(WitDbCommand cmd, string from, string to, decimal amount, string status, string? error)
{
cmd.CommandText = """
INSERT INTO Transactions (FromAccount, ToAccount, Amount, Status, ErrorMessage)
VALUES (@from, @to, @amount, @status, @error)
""";
cmd.Parameters.Clear();
cmd.Parameters.Add(new WitDbParameter("@from", from));
cmd.Parameters.Add(new WitDbParameter("@to", to));
cmd.Parameters.Add(new WitDbParameter("@amount", amount));
cmd.Parameters.Add(new WitDbParameter("@status", status));
cmd.Parameters.Add(new WitDbParameter("@error", error ?? (object)DBNull.Value));
cmd.ExecuteNonQuery();
}
static void ShowBalances(WitDbConnection conn)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT Name, Balance FROM Accounts ORDER BY Name";
Console.WriteLine("Account Balances:");
Console.WriteLine("─".PadRight(30, '─'));
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
Console.WriteLine(
Loading...
quot; {reader["Name"],-15} {reader.GetDecimal(1),12:C}");
}
}
static void ShowHistory(WitDbConnection conn)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = """
SELECT FromAccount, ToAccount, Amount, Status, Timestamp
FROM Transactions
ORDER BY Timestamp DESC
LIMIT 10
""";
Console.WriteLine("\nRecent Transactions:");
Console.WriteLine("─".PadRight(70, '─'));
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
var status = reader["Status"].ToString() == "SUCCESS" ? "✓" : "✗";
var timestamp = reader.GetDateTime(4).ToString("HH:mm:ss");
Console.WriteLine(
Loading...
quot; {status} [{timestamp}] {reader["FromAccount"]} → {reader["ToAccount"]}: {reader.GetDecimal(2):C}");
}
}
Sample output:
╔══════════════════════════════════════════════════════════════╗
║ Bank Transfer System ║
╚══════════════════════════════════════════════════════════════╝
Account Balances:
──────────────────────────────
Alice $1,000.00
Bob $500.00
Charlie $250.00
Performing transfers:
──────────────────────────────────────────────────
✓ Alice → Bob: $200.00
✓ Bob → Charlie: $50.00
✓ Charlie → Alice: $75.00
✗ Alice → Bob: $10,000.00 - Insufficient funds: Alice has $875.00, needs $10,000.00
Final balances:
Account Balances:
──────────────────────────────
Alice $875.00
Bob $650.00
Charlie $225.00
Recent Transactions:
──────────────────────────────────────────────────────────────────────
✗ [14:32:01] Alice → Bob: $10,000.00
✓ [14:32:01] Charlie → Alice: $75.00
✓ [14:32:01] Bob → Charlie: $50.00
✓ [14:32:01] Alice → Bob: $200.00
Transactions Summary
API
Begin Transaction
Commit
Rollback
ADO.NET
connection.BeginTransaction()
transaction.Commit()
transaction.Rollback()
EF Core
context.Database.BeginTransaction()
transaction.Commit()
transaction.Rollback()
Core API
db.BeginTransaction()
tx.Commit()
tx.Rollback()
Best practices:
- ✅ Always use transactions for multi-step operations
- ✅ Keep transactions short — don't hold them during user input
- ✅ Use
using to ensure proper disposal
- ✅ Use savepoints for complex operations with partial rollback needs
- ✅ Choose appropriate isolation level for your use case
- ✅ Enable MVCC for better concurrency
Quick Reference
A handy reference card for common WitDatabase operations. Bookmark this page!
Connection Strings
Common Patterns:
// Simple file database
"Data Source=app.witdb"
// In-memory (testing)
"Data Source=:memory:"
// With encryption
"Data Source=secure.witdb;Encryption=aes-gcm;Password=MySecret123!"
// ChaCha20 for Blazor WASM
"Data Source=app.witdb;Encryption=chacha20;Password=secret"
// Read-only mode
"Data Source=app.witdb;Mode=ReadOnly"
// With all options
"Data Source=app.witdb;Store=btree;Encryption=aes-gcm;Password=secret;MVCC=true;Isolation Level=Snapshot"
All Parameters
Parameter
Values
Default
Description
Data Source
path or :memory:
—
Database location
Mode
ReadOnly, ReadWrite, ReadWriteCreate
ReadWriteCreate
Access mode
Store
BTree, LSM
BTree
Storage engine
Encryption
None, aes-gcm, chacha20
None
Encryption algorithm
Password
string
—
Encryption password
User
string
—
User for key derivation
Fast Encryption
true, false
false
Reduced PBKDF2 iterations
MVCC
true, false
true
Multi-version concurrency
Isolation Level
ReadUncommitted, ReadCommitted, RepeatableRead, Snapshot, Serializable
ReadCommitted
Default isolation
Parallel Mode
None, Auto, Latched, Buffered
None
Concurrency mode
Cache Size
integer
1000
Pages in cache
Page Size
integer
4096
Bytes per page
Pooling
true, false
true
Connection pooling
Core API (Key-Value)
using OutWit.Database.Core.Builder;
// Create / Open
using var db = WitDatabase.CreateInMemory();
using var db = WitDatabase.Create("app.witdb");
using var db = WitDatabase.Open("app.witdb");
using var db = WitDatabase.CreateOrOpen("app.witdb");
using var db = WitDatabase.CreateOrOpen("app.witdb", "password");
// Builder pattern
using var db = new WitDatabaseBuilder()
.WithFilePath("app.witdb")
.WithBTree() // or .WithLsmTree()
.WithEncryption("password") // or .WithBouncyCastleEncryption()
.WithTransactions()
.WithMvcc()
.Build();
// Basic operations
db.Put(key, value); // Store
var value = db.Get(key); // Retrieve (null if not found)
db.Delete(key); // Remove
bool exists = db.Exists(key); // Check existence
// Async versions
await db.PutAsync(key, value);
var value = await db.GetAsync(key);
await db.DeleteAsync(key);
// Scanning
foreach (var (k, v) in db.Scan(startKey, endKey))
{
// Process key-value pairs in order
}
// Transactions
using var tx = db.BeginTransaction();
tx.Put(key1, value1);
tx.Put(key2, value2);
tx.Commit(); // or tx.Rollback();
ADO.NET
Connection
using OutWit.Database.AdoNet;
using var connection = new WitDbConnection("Data Source=app.witdb");
connection.Open();
// ... use connection
// Automatically closed and disposed
Execute Commands
using var cmd = connection.CreateCommand();
// Non-query (CREATE, INSERT, UPDATE, DELETE)
cmd.CommandText = "CREATE TABLE Users (Id INTEGER PRIMARY KEY, Name TEXT)";
int rowsAffected = cmd.ExecuteNonQuery();
// Scalar (single value)
cmd.CommandText = "SELECT COUNT(*) FROM Users";
var count = cmd.ExecuteScalar();
// Reader (multiple rows)
cmd.CommandText = "SELECT * FROM Users";
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
var id = reader.GetInt32(0);
var name = reader["Name"].ToString();
}
Parameters
cmd.CommandText = "INSERT INTO Users (Name, Email) VALUES (@name, @email)";
cmd.Parameters.Add(new WitDbParameter("@name", "Alice"));
cmd.Parameters.Add(new WitDbParameter("@email", "alice@example.com"));
cmd.ExecuteNonQuery();
// Clear for reuse
cmd.Parameters.Clear();
Transactions
using var transaction = connection.BeginTransaction();
try
{
using var cmd = connection.CreateCommand();
cmd.Transaction = transaction;
cmd.CommandText = "UPDATE Accounts SET Balance = Balance - 100 WHERE Id = 1";
cmd.ExecuteNonQuery();
cmd.CommandText = "UPDATE Accounts SET Balance = Balance + 100 WHERE Id = 2";
cmd.ExecuteNonQuery();
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
Async
await connection.OpenAsync();
using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT * FROM Users";
await using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
// Process row
}
Entity Framework Core
DbContext Setup
using Microsoft.EntityFrameworkCore;
using OutWit.Database.EntityFramework;
public class AppDbContext : DbContext
{
public DbSet<User> Users => Set<User>();
public DbSet<Post> Posts => Set<Post>();
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseWitDb("Data Source=app.witdb");
}
// With DI (ASP.NET Core)
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseWitDb("Data Source=app.witdb"));
CRUD Operations
using var context = new AppDbContext();
context.Database.EnsureCreated();
// Create
context.Users.Add(new User { Name = "Alice" });
await context.SaveChangesAsync();
// Read
var users = await context.Users.ToListAsync();
var user = await context.Users.FindAsync(1);
var alice = await context.Users.FirstOrDefaultAsync(u => u.Name == "Alice");
// Update
user.Name = "Alice Smith";
await context.SaveChangesAsync();
// Delete
context.Users.Remove(user);
await context.SaveChangesAsync();
Common LINQ Queries
// Filter
var active = context.Users.Where(u => u.IsActive);
// Sort
var sorted = context.Users.OrderBy(u => u.Name);
var sortedDesc = context.Users.OrderByDescending(u => u.CreatedAt);
// Select specific columns
var names = context.Users.Select(u => new { u.Id, u.Name });
// Pagination
var page = context.Users
.OrderBy(u => u.Id)
.Skip(20)
.Take(10);
// Aggregates
var count = await context.Users.CountAsync();
var any = await context.Users.AnyAsync(u => u.IsAdmin);
var max = await context.Users.MaxAsync(u => u.Age);
// Include related data
var usersWithPosts = context.Users
.Include(u => u.Posts)
.ToList();
// Group
var byCountry = context.Users
.GroupBy(u => u.Country)
.Select(g => new { Country = g.Key, Count = g.Count() });
Transactions
// Implicit (single SaveChanges)
context.Users.Add(user1);
context.Users.Add(user2);
await context.SaveChangesAsync(); // All or nothing
// Explicit
using var transaction = await context.Database.BeginTransactionAsync();
try
{
// Multiple operations
await context.SaveChangesAsync();
await context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
SQL Reference
Data Types
SQL Type
C# Type
Notes
INTEGER
int, long
64-bit
REAL
double
64-bit float
TEXT
string
UTF-8
BLOB
byte[]
Binary
BOOLEAN
bool
0/1
DATETIME
DateTime
ISO 8601
DECIMAL(p,s)
decimal
Exact numeric
VARCHAR(n)
string
Max length
GUID
Guid
UUID
DDL (Data Definition)
-- Create table
CREATE TABLE Users (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Name VARCHAR(100) NOT NULL,
Email VARCHAR(255) UNIQUE,
Age INTEGER DEFAULT 0,
IsActive BOOLEAN DEFAULT TRUE,
CreatedAt DATETIME DEFAULT NOW()
);
-- Create index
CREATE INDEX idx_users_email ON Users(Email);
CREATE UNIQUE INDEX idx_users_name ON Users(Name);
-- Drop
DROP TABLE IF EXISTS Users;
DROP INDEX IF EXISTS idx_users_email;
-- Alter (limited support)
ALTER TABLE Users ADD COLUMN Phone VARCHAR(20);
DML (Data Manipulation)
-- Insert
INSERT INTO Users (Name, Email) VALUES ('Alice', 'alice@example.com');
INSERT INTO Users (Name, Email) VALUES ('Bob', 'bob@example.com') RETURNING Id;
-- Select
SELECT * FROM Users;
SELECT Id, Name FROM Users WHERE IsActive = TRUE ORDER BY Name LIMIT 10;
SELECT COUNT(*), Country FROM Users GROUP BY Country HAVING COUNT(*) > 5;
-- Update
UPDATE Users SET Name = 'Alice Smith' WHERE Id = 1;
UPDATE Users SET IsActive = FALSE WHERE LastLogin < '2024-01-01' RETURNING Id, Name;
-- Delete
DELETE FROM Users WHERE Id = 1;
DELETE FROM Users WHERE IsActive = FALSE RETURNING Id;
-- Upsert
INSERT INTO Users (Id, Name) VALUES (1, 'Alice')
ON CONFLICT(Id) DO UPDATE SET Name = excluded.Name;
Joins
-- Inner join
SELECT u.Name, p.Title
FROM Users u
INNER JOIN Posts p ON u.Id = p.UserId;
-- Left join
SELECT u.Name, COUNT(p.Id) as PostCount
FROM Users u
LEFT JOIN Posts p ON u.Id = p.UserId
GROUP BY u.Id;
Common Functions
-- String
LENGTH(str), UPPER(str), LOWER(str), TRIM(str)
SUBSTR(str, start, length), REPLACE(str, old, new)
CONCAT(str1, str2), INSTR(str, substr)
-- Numeric
ABS(n), ROUND(n, decimals), CEIL(n), FLOOR(n)
MIN(n), MAX(n), SUM(n), AVG(n), COUNT(*)
-- Date/Time
NOW(), DATE('now'), TIME('now')
STRFTIME('%Y-%m-%d', datetime_col)
-- Null handling
COALESCE(val1, val2, ...), IFNULL(val, default), NULLIF(val1, val2)
-- Conditional
CASE WHEN condition THEN result ELSE default END
IIF(condition, true_val, false_val)
Common Patterns
"I want to..." Quick Guide
I want to...
Solution
Create a simple database
WitDatabase.CreateOrOpen("app.witdb")
Use SQL queries
Install OutWit.Database.AdoNet, use WitDbConnection
Use EF Core
Install OutWit.Database.EntityFramework, use UseWitDb()
Encrypt my database
Add Encryption=aes-gcm;Password=secret to connection string
Use in Blazor WASM
Install .Core.IndexedDb, use WithIndexedDbStorage()
Insert many rows fast
Wrap in transaction, use Prepare()
Handle concurrent access
Add Parallel Mode=Auto to connection string
Use in unit tests
Use Data Source=:memory: for isolated in-memory DB
Store JSON objects
Serialize to byte[] with System.Text.Json
Get last inserted ID
Use RETURNING Id clause or LAST_INSERT_ROWID()
Error Handling
try
{
using var connection = new WitDbConnection(connectionString);
connection.Open();
// ... operations
}
catch (WitDbException ex) when (ex.Message.Contains("locked"))
{
// Database is locked by another process
Console.WriteLine("Database is busy, try again later");
}
catch (WitDbException ex) when (ex.Message.Contains("decrypt"))
{
// Wrong password
Console.WriteLine("Invalid password");
}
catch (WitDbException ex)
{
// Other database errors
Console.WriteLine(
Loading...
quot;Database error: {ex.Message}");
}
Testing Pattern
public class UserServiceTests
{
private AppDbContext CreateTestContext()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseWitDb("Data Source=:memory:")
.Options;
var context = new AppDbContext(options);
context.Database.EnsureCreated();
return context;
}
[Fact]
public async Task CreateUser_ShouldAssignId()
{
// Arrange
using var context = CreateTestContext();
var service = new UserService(context);
// Act
var user = await service.CreateUserAsync("Alice", "alice@test.com");
// Assert
Assert.True(user.Id > 0);
Assert.Equal("Alice", user.Name);
}
}
Repository Pattern
public interface IRepository<T> where T : class
{
Task<T?> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(T entity);
}
public class Repository<T> : IRepository<T> where T : class
{
private readonly AppDbContext _context;
private readonly DbSet<T> _dbSet;
public Repository(AppDbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public async Task<T?> GetByIdAsync(int id)
=> await _dbSet.FindAsync(id);
public async Task<IEnumerable<T>> GetAllAsync()
=> await _dbSet.ToListAsync();
public async Task AddAsync(T entity)
{
await _dbSet.AddAsync(entity);
await _context.SaveChangesAsync();
}
public async Task UpdateAsync(T entity)
{
_dbSet.Update(entity);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(T entity)
{
_dbSet.Remove(entity);
await _context.SaveChangesAsync();
}
}
Package Reference
Package
Purpose
Install
OutWit.Database.Core
Key-value API
dotnet add package OutWit.Database.Core
OutWit.Database.AdoNet
SQL + ADO.NET
dotnet add package OutWit.Database.AdoNet
OutWit.Database.EntityFramework
EF Core
dotnet add package OutWit.Database.EntityFramework
OutWit.Database.Core.IndexedDb
Blazor WASM
dotnet add package OutWit.Database.Core.IndexedDb
OutWit.Database.Core.BouncyCastle
ChaCha20
dotnet add package OutWit.Database.Core.BouncyCastle