Repository navigation
Core Concepts
Four ideas carry most of Birko. Learn these and the rest of the framework reads naturally.
A plain C# class. Inherit from one of three bases depending on what you need:
| Base | Gives you | Use for |
|---|---|---|
AbstractModel |
Guid id |
anything stored |
AbstractDatabaseModel |
+ SQL attributes support | SQL tables |
AbstractLogModel |
+ CreatedAt, UpdatedAt
|
rows you want timestamped |
public class Product : AbstractModel
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}No base class is required by the store interfaces themselves — but the bases give you the identity and timestamps that the decorators and repositories expect.
Settings say where a thing is and how to reach it. They form a chain, so each provider only adds what it actually needs:
Settings Location, Name
└─ PasswordSettings + Password
└─ RemoteSettings + UserName, Port, UseSecure
└─ SqlSettings + CommandTimeout, ConnectionTimeout
└─ MSSqlSettings / MySqlSettings / PostgreSqlSettings / TimescaleDBSettings
var settings = new SqLiteSettings(@"C:\data", "shop.db"); // file-based: location + name💡 A provider's settings class ships with the provider, not with Birko.Configuration —
SqLiteSettings is in Birko.Data.SQL.SqLite.Stores. Look in the provider's namespace.
Location:Name are treated as the same database. Connectors
are cached process-wide on that key, so a second settings object differing only in CommandTimeout
silently reuses the first one's connector.
A store is the thing you actually call. The same eight or so methods exist for every backend:
var id = store.Create(product); // returns the Guid
var one = store.Read(id);
var matches = store.Read(x => x.Price < 10m, null, null, null);
store.Update(product);
store.Delete(product);
var n = store.Count();Not Store with async methods — different classes:
var store = new SQLiteStore<Product>(); // sync
var asyncStore = new AsyncSQLiteStore<Product>(); // async: CreateAsync, ReadAsync, …Collections and filter-based writes:
store.Create(new[] { a, b, c }); // create many
store.Delete(x => x.Price > 100m); // delete by filter
store.Update(x => x.Discontinued, p => p.Price = 0m); // update by filterRead(filter) returns the collection, not one item. The bulk overload hides
the single-result one. For a single match use ReadFirst(filter).
Delete(x => true) is the
explicit "yes, everything" door; a filter that merely reduces to everything throws
WholeTableWriteException. This has caught real bugs — it is not paranoia.
using Birko.Data.Stores;
var page = store.Read(
filter: x => x.Price > 0m,
orderBy: OrderBy<Product>.By(p => p.Name), // .ByDescending(…) too
limit: 20,
offset: 40);You never call Init(). The first CRUD call creates the table or file if needed. That is why the
Getting Started example works with no setup step.
A store persists a model. A repository adds domain behaviour and can map between a view model (what your API exposes) and a model (what you store). If you don't need that, use stores directly — plenty of applications do.
IStore Create / Read / Update / Delete / Count
IBulkStore + collections and filter-based writes
IRepository the same, plus view-model mapping and domain rules
// 1. model
[Table("Products")]
public class Product : AbstractDatabaseModel
{
[MaxLengthField(200)] public string Name { get; set; } = string.Empty;
[PrecisionField(18), ScaleField(2)] public decimal Price { get; set; }
}
// 2. settings + 3. store
var store = new SQLiteStore<Product>();
store.SetSettings(new SqLiteSettings(@"C:\data", "shop.db"));
// 4. use it — the table is created on first call
store.Create(new Product { Name = "Widget", Price = 9.99m });Swap SQLiteStore + SqLiteSettings for MongoDBStore + its settings and nothing else changes.
- Data and Stores — every backend, and which to choose
- SQL Databases — attributes, indexes, providers, migrations
Start here
Data
Services
UI