Recipes
Task-first answers to "how do I do X in an app I already have?" Each recipe is the shortest path — the command, the one wiring line, and where to go deeper. The Tutorial teaches these in order on one app; this page is the lookup. Keep the Cheat sheet open alongside.
| I want to… | Jump to |
|---|---|
| add a CRUD feature to a database I already have | ↓ |
| relate two entities (one-to-many, many-to-many) | ↓ |
| keep deleted rows, or forbid deleting | ↓ |
| add a search box over a table | ↓ |
| know who is signed in, from anywhere | ↓ |
| keep each customer's data apart | ↓ |
| cache a query for the page, refreshed after writes | ↓ |
| require a login to reach a page | ↓ |
| run work off the request thread | ↓ |
| send a transactional email | ↓ |
| cache an expensive query | ↓ |
| keep a file a user uploads | ↓ |
| publish a domain event durably | ↓ |
| harden SQLite for production | ↓ |
| deploy, and redeploy | ↓ |
| test a feature | ↓ |
Add a feature to an existing database
Declare the aggregate under Features/Orders/ and its pages beside it. Nothing goes on the context — no
DbSet, no configuration class: RaskDbContext maps every aggregate you declare, and the build generates —
from the traits on its base — its form model (OrderModel), read face (OrderRead, queried as Order.Where(…)), Order.Find(id) + order.Save(), and writes (Order.Create(model)).
public sealed class Order : Aggregate<Guid, Timestamps, Versioned, Model, Deletable>
{
[MaxLength(40)]
public string Reference { get; private set; } = "";
}
rask db add AddOrder && rask db update
→ Reference: Rask.Data · Learn it: Tutorial Ch 3
Add a related entity
Another aggregate is referred to by its id, never held — a navigation from one aggregate to another is a build error (RASK087). The read face infers the join from the id's name:
public Guid CustomerId { get; private set; } // on Order
var mine = await Order.Where(o => o.Customer.Country == "HU"); // navigation, inferred
A part of the aggregate — an order's lines — is an Entity<TId> kept in a private list and changed only
through the root; loading the root by id loads it whole. Many-to-many between aggregates is a list of ids.
→ Reference: children · the read face · Learn it: Tutorial Ch 3
Keep deleted rows, or forbid deleting
Deletable on the base generates a delete that removes the row. Name another trait, or none, to say otherwise:
public sealed class Order : Aggregate<Guid, Timestamps, Versioned, Model, SoftDelete> { } // Order.Delete stamps DeletedAt; reads hide the row
public sealed class Order : Aggregate<Guid, Timestamps, Versioned, Model> { } // no Order.Delete at all — cancel it instead
Then rask db add OrderDeletes && rask db update. Order.IgnoreQueryFilters() brings soft-deleted rows
back into a read.
→ Reference: choosing what a table carries
Add a search box
Declare which text is searchable in the aggregate's static Configure, add a migration, and search the read
face — ranked, word-aware and accent-insensitive, on SQLite, in the browser and on PostgreSQL:
public static void Configure(EntityTypeBuilder<Product> builder) =>
builder.HasFullTextSearch(p => new { p.Name, p.Description }); // then: rask db add ProductSearch && rask db update
var hits = await Product.Search(query).Take(20);
await Product.Search(query).Skip(20).Take(20); // best match first, paged in SQL
→ Reference: full-text search
Know who is signed in
Current answers from anywhere — a static factory, a command handler, an API endpoint, a job — with nothing
injected:
public static Note Create(string text) => new() { Id = Guid.CreateVersion7(), Text = text, OwnerId = Current.RequiredUserId };
Current.UserId is null when nobody is signed in, and Current.RequiredUserId throws. A component that
must re-render on sign-in injects IUserProvider instead.
→ Reference: Current
Keep each customer's data apart
One trait partitions a table by tenant; every read filters to the signed-in user's tenant and every create stamps it:
public sealed class Invoice : Aggregate<Guid, Timestamps, Versioned, Model, Deletable, PerTenant> { }
using (Tenant.Use(tenantId)) { /* an admin working in one tenant */ }
→ Reference: multi-tenancy
Cache a query for the page
QueryClient.Query caches a read for the session, deduplicates it and refetches it in the background. Keyed
by the aggregate, it refreshes itself after any write to that aggregate — no invalidation to write:
var count = QueryClient.Query(QueryKey.For<Product>("count"), () => Product.Count());
→ Reference: Rask.Query · Learn it: Tutorial Ch 2
Require login on a page
Two gates, use both: [Authorize] at the route (redirects anonymous deep-links to /login), and the
Authorize component to hide UI that anonymous users shouldn't see.
[Authorize] // route-level; from Microsoft.AspNetCore.Authorization
public sealed partial class CreateProduct : Component { … }
Authorize[ NewProductButton() ] // rendered only for signed-in users
Authorize.Role("admin")[ DeleteProductButton(product.Id) ]
The login page itself is already there: /login, /register and /logout are built in, and you
replace any of them by declaring your own page at the same route.
→ Reference: authentication · Learn it: Tutorial Ch 3
Run work off the request thread
Write a job record and handler, add one registration + its table, then enqueue. The enqueue returns as soon as the row is written, so the request finishes immediately; a background processor runs it at-least-once.
public sealed record SendOrderReceipt(Guid OrderId) : IJob;
builder.Services.AddRaskJobs<ProductsDbContext>(o => { /* … */ }); // needs AddRaskCqrs()
modelBuilder.AddRaskJobs(); // then: rask db add AddJobs && rask db update
await Jobs.Enqueue(new SendOrderReceipt(order.Id)); // or: .In(24.Hours)
→ Reference: background jobs · Learn it: Tutorial Ch 4
Send a transactional email
Write an email whose body is a Rask component, add the mail queue, then send. Delivery happens off the request thread over SMTP with backoff.
builder.Services.AddRaskMail<ProductsDbContext>(o => { /* SMTP … */ });
modelBuilder.AddRaskMail(); // then: rask db add AddMail && rask db update
→ Reference: transactional email · Learn it: Tutorial Ch 5
Cache an expensive query
One registration + one table, then remember the read and forget it on write.
builder.Services.AddRaskCache<ProductsDbContext>();
modelBuilder.AddRaskCache(); // then: rask db add AddCache && rask db update
var products = await Cache.Remember("products", LoadProducts).For(10.Minutes);
await Cache.Forget("products"); // when the catalog changes
→ Reference: cache · Learn it: Tutorial Ch 6
Keep an uploaded file
Save the IRaskFile from the picker's handler with Files.Save, keep the returned id on your entity, and link
to it. A RaskApp already has storage on; a hand-wired host adds the registration, the table and the routes.
builder.Services.AddRaskStorage<ProductsDbContext>();
modelBuilder.AddRaskStorage(); // then: rask db add AddStorage && rask db update
app.MapRaskStorage(); // after app.MapRask<App>()
var saved = await Files.Save(file.OpenReadStream, file.Name, file.Size).Public();
product.SetPhoto(saved.Id);
Img.Src(Files.Url(saved.Id)).Alt(product.Name) // no I/O — safe inside Render
Files on the default disk provider are archived by rask db backup beside the database, but not by Litestream
or snapshots; use S3 or Azure for uploads you can't afford to lose.
→ Reference: file storage · The picker: HTTP & files
Publish a domain event through the outbox
A handler that must not be lost is durable: its outbox row is written in the same transaction as your data,
and it runs after the commit (crash-safe, at-least-once). Raise the event from the aggregate, and let the handler
choose. A RaskApp has the outbox on already:
[LocalOnly] public sealed record OrderCreated(Guid Id) : IEvent; // then Raise(new OrderCreated(Id)) in Create
public sealed class NotifyWarehouse : IDurableHandler<OrderCreated> { … } // via the outbox, retried
public sealed class RefreshTotals : IEventHandler<OrderCreated> { … } // in memory, at once
→ Reference: outbox · Learn it: Tutorial Ch 7
Turn on production SQLite
UseRaskSqlite is a drop-in for .UseSqlite that installs the pragma interceptor (WAL, foreign_keys,
busy_timeout on every open). Add Litestream for continuous off-box backup.
.UseRaskSqlite(sp) // was .UseSqlite("Data Source=app.db"); reads Rask:ConnectionStrings:App
builder.Services.AddRaskSqliteLitestream(); // reads Rask:Litestream — set ReplicaUrl to turn it on
→ Reference: production SQLite · Learn it: Tutorial Ch 8
Deploy and redeploy
rask deploy builds your Docker image on the server, runs it behind a shared Caddy proxy with
automatic HTTPS, and does a health-gated zero-downtime swap. The first run on a bare box also sets it
up (Docker, a non-root deploy user, firewall, SSH hardening).
rask deploy --host root@your-box.example.com --domain shop.example.com # first time
rask deploy # after: host/domain remembered
rask deploy --github-actions # write .github/workflows/deploy.yml
Needs a Dockerfile — rask new writes one.
→ Reference: deployment · Learn it: Tutorial Ch 11
Test a feature
Add a sibling <Project>.Tests project and test the slice directly — the domain rules on the entity, and
a SQLite round-trip through the real DbContext:
[Fact]
public void Create_sets_the_fields()
{
var product = Product.Create("Desk", 249m, inStock: true);
Assert.Equal("Desk", product.Name);
Assert.NotEqual(Guid.Empty, product.Id);
}
→ Reference: testing
Command reference → the rask CLI · One-page reference → Cheat sheet ·
Learn it in order → Tutorial