This page builds one small thing and finishes it: a model, a database file, rows written, rows read back. It uses Entity Framework Core, since that is how most projects will reach for WitDatabase. The same work is shown afterwards in ADO.NET and in the core API, so you can see what each layer costs you.
You will need OutWit.Database.EntityFramework. See installation if
you have not added it yet.
A model and a context
Nothing here is specific to WitDatabase. It is an ordinary EF Core model and an ordinary
DbContext.
using Microsoft.EntityFrameworkCore;
public class Note
{
public long Id { get; set; }
public string Title { get; set; } = "";
public string Body { get; set; } = "";
public DateTime CreatedAt { get; set; }
}
public class NotesContext : DbContext
{
public DbSet<Note> Notes => Set<Note>();
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseWitDb("Data Source=notes.witdb");
}UseWitDb is the only line that names the provider. Swap it for UseNpgsql, UseSqlServer,
UseMySql or whichever provider your production database uses, and the rest of this file compiles
unchanged. That property is what the whole engine is built around.
Creating the database and writing to it
using var context = new NotesContext();
context.Database.EnsureCreated();
context.Notes.Add(new Note
{
Title = "First note",
Body = "The database created itself when EnsureCreated ran.",
CreatedAt = DateTime.UtcNow
});
context.SaveChanges();EnsureCreated builds the schema from the model and is the quickest way to get moving. For anything
that will outlive an afternoon, use migrations instead: dotnet ef migrations add Initial followed
by context.Database.Migrate(). The provider supports the full migration pipeline, including
scaffolding an existing database back into a model.
Reading it back
using var context = new NotesContext();
var recent = context.Notes
.Where(note => note.CreatedAt > DateTime.UtcNow.AddDays(-7))
.OrderByDescending(note => note.CreatedAt)
.Take(20)
.ToList();
foreach (var note in recent)
Console.WriteLine($"{note.CreatedAt:u} {note.Title}");The Where, OrderByDescending and Take are translated to SQL and executed by the engine rather
than evaluated in memory afterwards.
The same database, in memory, for tests
var options = new DbContextOptionsBuilder<NotesContext>()
.UseWitDbInMemory()
.Options;
using var context = new NotesContext(options);
context.Database.EnsureCreated();No file, no path, no cleanup between test runs, and every call makes its own database so two fixtures in one suite cannot see each other.
Use UseWitDbInMemory() rather than UseWitDb("Data Source=:memory:"). An in-memory database is
private to the connection that opened it, and EF Core opens and closes a connection for every
operation, so a connection string hands each operation a fresh empty database: EnsureCreated
returns true and the next SaveChanges fails with Table not found. UseWitDbInMemory() opens a
connection and keeps it, which is the same recipe SQLite's provider documents and for the same
reason.
That connection lives as long as the options object. When the lifetime has to be in your hands,
open a WitDbConnection yourself and pass it to the overload that takes one.
Constraints are enforced, transactions roll back, SQL is parsed and planned, and a query that would fail against your production server fails here too. EF Core's in-memory provider enforces none of that and will accept a save a real database rejects.
The same work in ADO.NET
If you would rather write the SQL yourself, drop a layer. Everything below comes from
OutWit.Database.AdoNet, which the EF Core package already brought in.
using OutWit.Database.AdoNet;
using var connection = new WitDbConnection("Data Source=notes.witdb");
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = """
CREATE TABLE IF NOT EXISTS Notes (
Id BIGINT PRIMARY KEY AUTOINCREMENT,
Title VARCHAR(200) NOT NULL,
Body TEXT,
CreatedAt DATETIME NOT NULL
)
""";
command.ExecuteNonQuery();
command.CommandText = """
INSERT INTO Notes (Title, Body, CreatedAt)
VALUES (@title, @body, @createdAt)
RETURNING Id
""";
command.Parameters.AddWithValue("@title", "First note");
command.Parameters.AddWithValue("@body", "Written through ADO.NET.");
command.Parameters.AddWithValue("@createdAt", DateTime.UtcNow);
var id = (long)command.ExecuteScalar()!;
Console.WriteLine($"Inserted note {id}");RETURNING hands back the generated key as part of the insert, so there is no second round trip to
find out what it was. Pass values as parameters rather than concatenating them into the statement.
Reading is the ordinary DbDataReader loop:
command.Parameters.Clear();
command.CommandText = "SELECT Id, Title, CreatedAt FROM Notes ORDER BY CreatedAt DESC";
using var reader = command.ExecuteReader();
while (reader.Read())
Console.WriteLine($"[{reader.GetInt64(0)}] {reader.GetString(1)}");When you have no use for SQL
Underneath the SQL engine is an ordered key-value store, and you can use it on its own. This is the right layer for a cache, a session store, or anything where a schema would only be in the way.
using OutWit.Database.Core.Builder;
using var db = WitDatabase.CreateOrOpen("cache.witdb");
db.Put("session:8f21"u8.ToArray(), "alice"u8.ToArray());
var value = db.Get("session:8f21"u8.ToArray());
Console.WriteLine(Encoding.UTF8.GetString(value!));Keys are ordered, so Scan over a key prefix gives you a range in sorted order, and transactions
work the same way they do above.
What ended up on disk
notes.witdb is one file. It holds the schema, the data, the indexes and the catalogue, and it
moves between machines by being copied. There is no directory of side files to keep together unless
you asked for the LSM store, which uses a folder instead.
WitDatabase Studio opens the file, shows the tables, runs queries against them, and
reports the format version and encryption state. That matters when a .witdb file is your
application's document format and a customer sends you one.
Where to go next
- Choosing an API, a proper look at the four entry points
- WitSQL reference, the language in one place
- Entity Framework Core provider, migrations, scaffolding, and what is supported
- Connection strings, encryption, read-only mode, storage engines