C# Binding Overview¶
The wlite NuGet package provides C# access to libwlite via P/Invoke. Define your schema in .wlite files and use it from .NET applications. The binding wraps the libwlite C library and exposes it through C# types with proper resource management using IDisposable, exception-based error handling, and idiomatic .NET conventions.
Prerequisites¶
Before installing the NuGet package, ensure the following dependencies are available on your system.
- .NET SDK 6.0 or later
- libwlite installed from source
- SQLite3 development libraries
Installing libwlite¶
Build and install libwlite from the repository:
Verify the installation:
Platform requirements¶
The binding uses P/Invoke to call into the native libwlite shared library. The library must be discoverable at runtime through the standard library search path.
| Platform | Library file | Location |
|---|---|---|
| Linux | libwlite.so |
/usr/local/lib |
| macOS | libwlite.dylib |
/usr/local/lib |
| Windows | wlite.dll |
PATH or application directory |
Installation¶
Add the wlite NuGet package to your project:
Or add the package reference directly to your project file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="wlite" Version="0.2.0" />
</ItemGroup>
</Project>
Verifying the installation¶
Quick start¶
The fastest way to get started is to load a model, open a database, run migrations, and execute queries. This section walks through a minimal example.
Create a model file¶
Create a file named app.wlite in your project directory:
table users {
id integer primary key
name text not null
email text not null unique
created_at text default (datetime('now'))
}
table posts {
id integer primary key
user_id integer not null references users(id)
title text not null
body text
published_at text
}
Full example¶
using Wlite;
class Program
{
static void Main()
{
using var model = Model.Load("app.wlite");
using var db = Database.Open("app.db");
db.Migrate(model);
using var insertStmt = db.Prepare(
"INSERT INTO users (name, email) VALUES (?, ?)"
);
insertStmt.Bind(1, "Alice");
insertStmt.Bind(2, "alice@example.com");
insertStmt.Step();
insertStmt.Reset();
insertStmt.Bind(1, "Bob");
insertStmt.Bind(2, "bob@example.com");
insertStmt.Step();
using var queryStmt = db.Prepare("SELECT id, name, email FROM users");
while (queryStmt.Step())
{
var id = queryStmt.ColumnInt64(0);
var name = queryStmt.ColumnText(1);
var email = queryStmt.ColumnText(2);
Console.WriteLine($"{id}: {name} <{email}>");
}
}
}
What happens in this example¶
Model.Loadreads and parses the.wliteschema fileDatabase.Opencreates or opens an SQLite database at the given pathdb.Migrateapplies the schema to the database, creating or altering tables as neededdb.Preparecompiles an SQL statement for reuseBindsets parameter values,Stepexecutes the statement, andResetrewinds it for the next iteration- Column access methods retrieve result data from the current row
Types¶
The binding exposes five core types in the Wlite namespace.
| C# Type | C Equivalent | Description |
|---|---|---|
Wlite.Database |
wlite_db |
Open database connection |
Wlite.Model |
wlite_model |
Loaded .wlite schema |
Wlite.Statement |
wlite_stmt |
Prepared SQL statement |
Wlite.Transaction |
wlite_tx |
Active database transaction |
Wlite.WliteException |
wlite_result |
Exception type for errors |
Database¶
The Database type manages a connection to an SQLite database. It provides methods for executing SQL, preparing statements, running migrations, and starting transactions.
The Database constructor is private. Use the static Database.Open method to create instances. The Open method throws WliteException if the database cannot be opened.
Key methods on Database:
Execute(string sql)runs a single SQL statement with no result rowsPrepare(string sql)returns aStatementfor parameterized queriesMigrate(Model model)applies a schema to the databaseBegin()starts a newTransactionQuery(string sql)returns aStatementfor reading resultsQueryScalar(string sql)returns a single value from a query
Model¶
The Model type represents a parsed .wlite schema file. It is immutable after loading and can be used to migrate multiple databases.
The Model type is created via the static Model.Load method. It holds a pointer to the native model handle and frees it on disposal. Models can be shared across threads since they are immutable after loading.
Statement¶
The Statement type wraps a prepared SQL statement. It supports parameter binding, step-by-step execution, and column access.
using var stmt = db.Prepare("SELECT * FROM users WHERE id = ?");
stmt.Bind(1, 42);
while (stmt.Step())
{
var name = stmt.ColumnText(1);
}
Statements are created by Database.Prepare or Database.Query. They are not thread-safe and should be used on a single thread.
Transaction¶
The Transaction type represents an active database transaction. It must be committed or rolled back before disposal.
Transactions are created by Database.Begin. If a transaction is disposed without being committed or rolled back, it is rolled back automatically.
WliteException¶
The WliteException type is thrown when any wlite operation fails. It carries a Result property of type WliteResult indicating the error code.
try
{
var db = Database.Open("/nonexistent/path/db.db");
}
catch (WliteException ex)
{
Console.WriteLine($"Error: {ex.Result} - {ex.Message}");
}
IDisposable pattern¶
All wlite types implement IDisposable. This ensures native resources are freed deterministically when you are done with them. The recommended pattern is to use using statements.
Using declarations¶
using var model = Model.Load("app.wlite");
using var db = Database.Open("app.db");
using var stmt = db.Prepare("SELECT * FROM users");
// All resources freed automatically at end of scope
Using blocks¶
using (var model = Model.Load("app.wlite"))
using (var db = Database.Open("app.db"))
{
db.Migrate(model);
// Use the database
}
// Both model and db are disposed here
Finalizer safety net¶
Each type includes a finalizer that calls Dispose. This ensures native memory is freed even if you forget to dispose manually. However, relying on finalizers is not recommended because:
- Finalizers run on the garbage collector thread at an unpredictable time
- Finalized objects require multiple garbage collection cycles to reclaim
- Native memory leaks can occur between the time you stop using an object and when the finalizer runs
Always use using statements for deterministic cleanup.
Preventing double disposal¶
Each type tracks whether it has been disposed. Calling Dispose more than once is safe and has no effect. The Dispose method uses a _disposed flag to prevent double-free of native handles.
Resource lifecycle¶
The order of disposal matters. Always dispose child resources before parent resources. In practice, this means disposing statements before the database, and the database before the model.
using var stmt = db.Prepare("SELECT 1");
// stmt is disposed first
using var db = Database.Open("app.db");
// db is disposed second
using var model = Model.Load("app.wlite");
// model is disposed last
With using declarations, disposal order follows reverse declaration order. Declare resources in the order they depend on each other.
Complete lifecycle example¶
using Wlite;
void ProcessData(string modelPath, string dbPath)
{
using var model = Model.Load(modelPath);
using var db = Database.Open(dbPath);
db.Migrate(model);
using var tx = db.Begin();
bool committed = false;
try
{
using var stmt = db.Prepare(
"INSERT INTO users (name, email) VALUES (?, ?)"
);
stmt.Bind(1, "Alice");
stmt.Bind(2, "alice@example.com");
stmt.Step();
stmt.Reset();
stmt.Bind(1, "Bob");
stmt.Bind(2, "bob@example.com");
stmt.Step();
tx.Commit();
committed = true;
}
finally
{
if (!committed)
{
tx.Rollback();
}
}
}
// stmt disposed, then tx, then db, then model
Thread safety¶
Models are immutable after loading and can be shared across threads safely. Database connections are not thread-safe. Each thread should use its own Database instance.
using Wlite;
using System.Threading.Tasks;
void ConcurrentWork()
{
using var model = Model.Load("app.wlite");
var tasks = new Task[4];
for (int i = 0; i < 4; i++)
{
int workerId = i;
tasks[i] = Task.Run(() =>
{
using var db = Database.Open("app.db");
db.Migrate(model);
for (int j = 0; j < 100; j++)
{
db.Execute(
"INSERT INTO work_items (thread_id, data) VALUES (?, ?)",
workerId, $"item_{j}"
);
}
});
}
Task.WaitAll(tasks);
}
Complete example¶
Here is a complete program demonstrating all major features:
using Wlite;
using System;
using System.Collections.Generic;
public class User
{
public long Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public bool Active { get; set; }
}
public class UserDatabase : IDisposable
{
private readonly Model _model;
private readonly Database _db;
public UserDatabase(string modelPath, string dbPath)
{
_model = Model.Load(modelPath);
_db = Database.Open(dbPath);
_db.Migrate(_model);
}
public void Dispose()
{
_db?.Dispose();
_model?.Dispose();
}
public void CreateTables()
{
_db.Execute(@"
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
active INTEGER DEFAULT 1,
created_at TEXT DEFAULT (datetime('now'))
)
");
}
public void InsertUser(string name, string email)
{
using var stmt = _db.Prepare(
"INSERT INTO users (name, email) VALUES (?, ?)"
);
stmt.Bind(1, name);
stmt.Bind(2, email);
stmt.Step();
}
public void InsertUsers(IEnumerable<(string Name, string Email)> users)
{
using var stmt = _db.Prepare(
"INSERT OR IGNORE INTO users (name, email) VALUES (?, ?)"
);
foreach (var (name, email) in users)
{
stmt.Bind(1, name);
stmt.Bind(2, email);
stmt.Step();
stmt.Reset();
}
}
public User GetUser(long id)
{
using var stmt = _db.Prepare(
"SELECT id, name, email, active FROM users WHERE id = ?"
);
stmt.Bind(1, id);
if (stmt.Step())
{
return new User
{
Id = stmt.ColumnInt64(0),
Name = stmt.ColumnText(1),
Email = stmt.ColumnText(2),
Active = stmt.ColumnInt64(3) != 0,
};
}
return null;
}
public List<User> ListUsers()
{
using var stmt = _db.Prepare(
"SELECT id, name, email, active FROM users ORDER BY name"
);
var users = new List<User>();
while (stmt.Step())
{
users.Add(new User
{
Id = stmt.ColumnInt64(0),
Name = stmt.ColumnText(1),
Email = stmt.ColumnText(2),
Active = stmt.ColumnInt64(3) != 0,
});
}
return users;
}
public long CountUsers()
{
return _db.QueryScalar("SELECT COUNT(*) FROM users");
}
}
class Program
{
static void Main()
{
using var db = new UserDatabase("app.wlite", "app.db");
db.CreateTables();
var users = new (string Name, string Email)[]
{
("Alice", "alice@example.com"),
("Bob", "bob@example.com"),
("Charlie", "charlie@example.com"),
};
db.InsertUsers(users);
Console.WriteLine($"Total users: {db.CountUsers()}");
Console.WriteLine();
Console.WriteLine("All users:");
foreach (var user in db.ListUsers())
{
var active = user.Active ? "" : " [inactive]";
Console.WriteLine($" {user.Id}: {user.Name} <{user.Email}>{active}");
}
var alice = db.GetUser(1);
if (alice != null)
{
Console.WriteLine();
Console.WriteLine($"Got user: {alice.Name}");
}
}
}