Rask
Rask
v0.23.1-alpha.0.496
You're reading the Rask v0.23.1-alpha.0.496 guides. Updated .View source
All guides

Chapter 3 — A second feature, and locking it down

Goal: add an Orders feature that shares the same database, then require a login to edit the catalog. You'll write: a second slice under Features/Orders/, sharing the one database.

1. A second feature in the same database

The One Person Framework idea is one database for the whole product — so a second feature does not get a second DbContext. It is mapped through the one the app already has.

Features/Orders/Order.cs is chapter 2's shape with different fields:

using System.ComponentModel.DataAnnotations;

namespace Shop.Features.Orders;

public sealed class Order : Aggregate<Guid, Timestamps, Versioned, Model, Deletable>
{
    [Range(0, 1_000_000)]
    public decimal Total { get; private set; }

    public Guid ProductId { get; private set; }

    public DateTime Placed { get; private set; }
}

Order reads and writes the way Product does (Order.Where(…), Order.Create(model), Order.AsQueryable()) and gets its own generated OrderModel, with nothing to build.

Look at ProductId. That is how an aggregate points at another one — an id, never a navigation, which is what stops a save here reaching into the catalogue (RASK087 makes it a compile error). You lose nothing by it: because Product is an aggregate with a Guid key, the build reads that id and puts the join on the read face for you, named after the property:

await Order.Where(o => o.Product.Name == "Anvil");   // declared nothing

Then the same four components as before: CreateOrder, UpdateOrder, DeleteOrder and OrdersPage. They're the chapter 2 files with Product swapped for Order, the routes moved under /orders, and the three inputs and table columns changed to Total, ProductId and Placed. Copy them and change the names.

ProductId is a Guid, not a Product. One aggregate refers to another by id: each is loaded and saved on its own, so an order never drags a product along with it.

What ties the slice to the existing database: nothing you write. RaskAppDbContext maps Order exactly as it maps Product, with no DbSet to add. That's the whole of "sharing a database": one context, one connection string, one migration history, however many features you add. Nothing else in the slice knows or cares.

Relating aggregates. Order.ProductId is a plain foreign key here. To have EF understand it as a relationship, give Order a static Configure — the place for any mapping rule the conventions don't cover (it needs using Microsoft.EntityFrameworkCore;, using Microsoft.EntityFrameworkCore.Metadata.Builders; and using Shop.Features.Products;):

public static void Configure(EntityTypeBuilder<Order> builder) =>
    builder.HasOne<Product>().WithMany().HasForeignKey(x => x.ProductId);

It is found by its signature and runs after Rask's conventions. See Rask.Data for the full relationship shapes.

Migrate

A new table means a new migration:

rask db add AddOrder

Run rask dev (restart it if it is still running — the app applies pending migrations as it starts) and browse to /orders — a second working CRUD feature, in the same app.db.

2. Require a login to edit the catalog

Right now anyone can create or delete products. The app already has accounts and a login; let's use them. Rask offers two gates, and you'll use both:

  • [Authorize] on a page — route-level. An anonymous deep-link to the page gets redirected to /login.
  • The Authorize component — content-level. It renders one of its slots depending on who's signed in, without leaving the page.

Gate the write pages

Add [Authorize] (from Microsoft.AspNetCore.Authorization) to the two write pages — Features/Products/CreateProduct.cs and UpdateProduct.cs:

using Microsoft.AspNetCore.Authorization;

[Authorize]                          // ← anonymous users are redirected to /login
[Route("/products/new")]
public sealed partial class CreateProduct : Component { … }

Leave the read-only ProductsPage (/products) public so shoppers can browse. DeleteProduct isn't a page — it has no URL anyone could deep-link to — so it's gated the other way, by not rendering it.

Hide the "New / Edit / Delete" buttons from anonymous users

Route gating stops direct navigation, but you also don't want to show buttons that will just bounce to the login page. Wrap them in the Authorize component (from Rask.Core.Components):

Div.Class("flex flex-wrap items-center gap-3")[
    Ui.Heading.Level(1).Xl["Products"],
    Ui.Spacer,
    Authorize[                             // only rendered for signed-in users
        Ui.Button.Primary.Href(Routes.CreateProduct())["New product"]
    ]
]

For role-specific bits — say a "Delete" button only admins should see — pass Roles. In the table's last cell that is:

Authorize.Role("admin")[
    DeleteProduct.Id(p.Id).Version(p.Version)
]

A button that is never rendered has no click handler on the page, so hiding it is a real gate on a live page, not just a cosmetic one.

Verify

  • /orders renders and creating an order persists it (same app.db as products).
  • Signed out, visiting /products/new redirects to /login; the "New product" button isn't shown on /products.
  • After signing in (register an account first at /register — the first one is the administrator), the create/edit/delete pages and buttons appear and work.

Learn more: authentication · Rask.Data · the rask CLI

Next → Chapter 4: Background jobs

Confirm