Skip to content

Adding Your First Feature

This guide walks you through adding a “Book” feature to BlazorSaas—a simple catalog entry with a title, author, and price. It covers defining the domain, persisting it, exposing it via GraphQL, and rendering it in a Blazor page.

Persistence

Define the Book entity in your domain layer to represent the feature’s core data.

Domain

Add a class with key properties, inheriting from AggregateRoot:

public class Book : AggregateRoot
{
public string Title { get; set; }
public string Author { get; set; }
public decimal Price { get; set; }
}

This serves as the aggregate root for book-related operations.

Schema

Store the Book entity using MartenDB by registering it in CoreRegistry.

Update the existing registry:

public class CoreRegistry : MartenRegistry
{
public CoreRegistry()
{
For<Product>().SingleTenanted();
For<Book>();
}
}

Generate Code

Run the code Nuke target to generate MartenDB and Wolverine code for Book.

Execute:

Terminal window
nuke code

This updates the internal directory with necessary files—commit them to source control.

API/GraphQL

Expose the Book entity through Hot Chocolate’s GraphQL API with a node, query, and mutation.

Node

Define a GraphQL node type for Book:

[ObjectType<Book>]
public static partial class BookNode
{
static partial void Configure(IObjectTypeDescriptor<Book> descriptor)
{
descriptor.BindFieldsExplicitly();
descriptor.Field(b => b.Id);
descriptor.Field(b => b.Title);
descriptor.Field(b => b.Author);
descriptor.Field(b => b.Price);
}
}

Mutation

Add a mutation to create books:

[Mutation]
[Authorize]
public static async Task<Book> CreateBook(
string title,
string author,
decimal price,
[Service] IDocumentSession session,
CancellationToken cancellationToken)
{
var book = new Book { Title = title, Author = author, Price = price };
session.Store(book);
await session.SaveChangesAsync(cancellationToken);
return book;
}

Query

Add a query to fetch books:

[Query]
public static IQueryable<Book> GetBooks(IQuerySession session)
{
return session.Query<Book>();
}

Generate Schema

Update the GraphQL schema for the web project with the schema Nuke target.

Run:

Terminal window
nuke schema

This syncs the API changes to the client.

Server Side

Create a Blazor page to display books using SSR.

Add Page

Add Pages/Books.razor:

@page "/books"
@inject IApiClient ApiClient
<h3>Book Catalog</h3>
@if (isLoading)
{
<Spinner Color="SpinnerColor.Primary"/>
}
else if (errors.Any())
{
<ErrorAlerts Errors="@errors"/>
}
else
{
<ul>
@foreach (var book in books)
{
<li>@book.Title by @book.Author - [email protected]</li>
}
</ul>
<NewBookForm />
}
@code {
private IReadOnlyList<IBook> books { get; set; } = [];
private IReadOnlyList<IClientError> errors { get; set; } = [];
private bool isLoading = true;
protected override async Task OnInitializedAsync()
{
var result = await ApiClient.BooksPage.ExecuteAsync();
if (result.IsSuccessResult())
{
books = result.Data!.Books;
}
else
{
errors = result.Errors;
}
isLoading = false;
}
}

Use API Client

Leverage Strawberry Shake’s generated client for the SSR query.

Define the query in pages.graphql:

query BooksPage {
books {
... Book
}
}

Add the fragment to fragments.graphql:

fragment Book on Book {
title
author
price
}

The client (IApiClient) is auto-generated after running schema.

Add Interactive Components

Add an interactive form to create books.

Create an interactive NewBookForm.razor with the mutation:

<EditForm Model="newBook" OnValidSubmit="HandleSubmit">
<InputText @bind-Value="newBook.Title" />
<InputText @bind-Value="newBook.Author" />
<InputNumber @bind-Value="newBook.Price" />
<button type="submit">Add Book</button>
</EditForm>
@code {
private BookInput newBook { get; set; } = new();
private async Task HandleSubmit()
{
var result = await ApiClient.CreateBook.ExecuteAsync(newBook.Title, newBook.Author, newBook.Price);
if (result.IsSuccessResult())
{
// Refresh page
}
}
}

Use API Client (Interactive)

Define the mutation in mutations.graphql:

mutation CreateBook($title: String!, $author: String!, $price: Decimal!) {
createBook(title: $title, author: $author, price: $price) {
title
}
}

The IApiClient.CreateBook method triggers it from the interactive component.

Preparing for Deployment

Generate Patch Migration

Create a migration script for schema changes with the patch-migration target.

Run:

Terminal window
nuke patch-migration

This generates a script in the repo for the Book addition.

Tag Commit for DB Versioning

Tag the commit with a version using MinVer.

Execute:

Terminal window
git tag v1.1.0

Apply locally:

Terminal window
nuke apply-migrations

Push with Tags

Push the changes and tag to your repository.

Run:

Terminal window
git push origin main --tags

Deployment will auto-apply the migration.