Data · CRUD
Catalog
Products and categories, read and written entirely on the client. SKU is unique, price is a
decimal, lead time is a TimeSpan, and discontinued-on is a nullable
DateOnly - the types a browser database is usually worst at. Every button below
is a normal EF Core call; the snippet under each panel is the code it runs.
Fastest persistent tier. Real files in the origin private file system. Concurrent connections, no COOP/COEP.
Products
The whole catalog, read with one query that includes each product's category. Click a row to edit it; the editor and the categories sit under the table.
| SKU | Name | Category | Price | Lead | Status | Actions |
|---|
Edit product
Select a row, or create one.
Categories
A category cascades to its products; a product on an order does not delete at all.
| Name | Items |
|---|
_categories = await context.Categories
.AsNoTracking()
.OrderBy(c => c.Name)
.ToListAsync();
_products = await context.Products
.AsNoTracking()
.Include(p => p.Category)
.OrderBy(p => p.Sku)
.ToListAsync();Inserting and updating
One code path for both: EF decides insert or update from the key. The write goes through the worker and lands in the storage backend named in the bar above, so it is still there after a reload.
product.LeadTime = TimeSpan.FromDays(_leadDays);
if (!product.IsActive && product.DiscontinuedOn is null)
{
product.DiscontinuedOn = DateOnly.FromDateTime(DateTime.UtcNow);
}
if (product.Id == 0)
{
context.Products.Add(product);
}
else
{
context.Products.Update(product);
}
// Async only. SaveChanges() throws BsqliteSynchronousApiNotSupportedException.
await context.SaveChangesAsync();
context.ChangeTracker.Clear();Foreign keys are enforced, not simulated
The engine in the worker is real SQLite with foreign keys on. Nothing in the browser
re-implements referential integrity, so a constraint failure comes back as the
DbUpdateException you would get on a server.
// OrderLine -> Product is DeleteBehavior.Restrict, so deleting a product that is on an
// order raises SQLITE_CONSTRAINT_FOREIGNKEY inside the worker and surfaces as a
// DbUpdateException here - foreign keys are enforced, not simulated.
var tracked = await context.Products.FirstAsync(p => p.Id == product.Id);
context.Products.Remove(tracked);
await context.SaveChangesAsync();The model that shapes these tables
Ordinary IEntityTypeConfiguration. The same class compiles against the
server's UseSqlite, and the same migration creates both databases -
nothing here is browser-specific.
CLR types stored in SQLite
SQLite has four storage classes. The ADO.NET binder decides which one each CLR value takes, so the worker never guesses and values round-trip as the type you wrote.
| CLR | SQLite | Where in this demo |
|---|---|---|
| decimal | TEXT + ef_* | Price, credit limit |
| DateTime | TEXT ISO | CreatedUtc |
| DateTimeOffset | TEXT + offset | OrderedAt |
| DateOnly | TEXT date | Date of birth, ship-by |
| TimeSpan | TEXT c | Lead time |
| Guid | TEXT | PublicId |
| bool | INTEGER 0/1 | IsActive, IsVip |
| enum | TEXT | Order status |
| double | REAL | Weight |