Repository navigation
Data and Stores
One store API, thirteen backends. Your model and your calls stay the same; only the store class and its settings change.
| Backend | Project | Good for |
|---|---|---|
| InMemory | Birko.Data.InMemory |
tests, prototypes. No settings, no persistence. Start here. |
| SQLite | Birko.Data.SQL.SqLite |
desktop apps, small services, local files |
| PostgreSQL | Birko.Data.SQL.PostgreSQL |
the default choice for a server application |
| MySQL | Birko.Data.SQL.MySQL |
when the database is already MySQL |
| SQL Server | Birko.Data.SQL.MSSql |
Microsoft estates |
| TimescaleDB | Birko.Data.TimescaleDB |
time series on PostgreSQL (hypertables) |
| MongoDB | Birko.Data.MongoDB |
documents, flexible shape |
| ElasticSearch | Birko.Data.ElasticSearch |
search, large read-heavy sets |
| RavenDB | Birko.Data.RavenDB |
documents with built-in indexing |
| CosmosDB | Birko.Data.CosmosDB |
Azure-hosted documents |
| InfluxDB | Birko.Data.InfluxDB |
metrics and time series |
| JSON | Birko.Data.JSON |
config-like data, human-readable files |
| XML | Birko.Data.XML |
legacy interop |
💡 Not sure? InMemory to learn, SQLite for a desktop or single-node app, PostgreSQL for a server. You can change later — that is the point of the abstraction.
// tests
var store = new InMemoryStore<Product>();
// local file
var store = new SQLiteStore<Product>();
store.SetSettings(new SqLiteSettings(@"C:\data", "shop.db"));
// document database
var store = new MongoDBStore<Product>();
store.SetSettings(new MongoDBSettings { /* host, port, database */ });Everything after that line is identical.
Filters are ordinary C# expressions, translated per backend:
store.Read(x => x.Price < 10m, null, null, null);
store.Read(x => x.Name.Contains("widget"), null, null, null);
store.Read(x => x.Price > 5m && x.Name.StartsWith("A"), null, null, null);
var ids = new List<Guid?> { id1, id2 }; // List, not array - and Guid? - see below
store.Read(x => ids.Contains(x.Guid), null, null, null);List<T>, not an array, for set membership. On .NET 9+ array.Contains(x) binds to
MemoryExtensions.Contains(ReadOnlySpan<T>, T), and a ref struct cannot appear in an expression tree,
so the filter will not compile. A List<T> binds to Enumerable.Contains and works.
(AbstractModel.Guid is Guid?, so the list element type is Guid? too.)
NotSupportedException rather than silently returning wrong rows — that is deliberate, and
several past bugs came from backends that used to fail quietly instead.
store.Create(new[] { a, b, c });
store.Delete(x => x.Discontinued); // delete by filter
store.Update(x => x.Discontinued, p => p.Price = 0m); // update by filter
store.DeleteAll(); // explicit "everything"Why DeleteAll() and not Delete()? A destructive all-rows operation gets a long, obvious name.
Delete() would sit one keystroke from Delete(items). Similarly, a filter that would match every
row throws WholeTableWriteException — say DeleteAll() if you mean it.
Wrap a store to add cross-cutting behaviour:
| Decorator | Adds |
|---|---|
| Timestamp | sets CreatedAt / UpdatedAt
|
| SoftDelete |
DeletedAt instead of removing rows |
| Audit | who changed what |
| Versioned | optimistic concurrency |
| Cached | read caching |
| Tenant | multi-tenant row scoping |
| Localization | per-culture text fields |
Built with StoreWrapperBuilder (Birko.Data.Composition). Order matters — a decorator that
suppresses a write also suppresses everything inside it.
-
Birko.Data.Migrations— versioned schema changes, per backend. See SQL Databases. -
Birko.Data.Sync— synchronise data between two stores, including bidirectional. -
Birko.Data.Views— read-only projections and aggregates. -
Birko.Data.Tenant— multi-tenancy, with an ambient tenant context. -
Birko.Data.Tagging— tag any entity. -
Birko.Data.EventSourcing— an event-sourced store.
- SQL Databases — attributes, indexes, providers, transactions
- Core Concepts — if stores, settings and models still feel unclear
Start here
Data
Services
UI