Skip to content

Basic Syntax

Zach Goethel edited this page May 27, 2025 · 3 revisions

This document outlines the basic syntax and structure of the DSL used for defining models, repositories, services, and components.

Models

Data structures are defined using a class-like syntax, specifying the fields and their types.

In Account.model:

schema {
  int Id,
  string Email = {""},
  string FirstName = {""},
  string LastName = {""},
  ...
}
  • schema: Indicates this is the model type's primary column definition.
  • int,string: Data types. Standard C# types are supported, and special values can be wrapped with { and }.
  • = {...}: Used to provide a default value expression, which can be any constant C# value

Partial Classes

You can extend data structures with partial classes to add functionality or specific properties.

In Account.model, specify a class Account.WithPassword:

partial WithPassword {
  string PasswordScheme = {""},
  string PasswordHash = {""},
  string PasswordSalt = {""},
  DateTime? PasswordSet
}
  • The partial keyword indicates an extension of a previously defined structure.
  • This allows you to add additional properties without modifying the base schema directly.

DTO Classes

You can add an inner class to the model which does not inherit from the base model class.

In Account.model, specify a class Account.LoginParams:

dto LoginParams {
  string Username = {""},
  string Password = {""},
  bool RememberMe = {true}
}
  • The dto keyword indicates this is a DTO inner class with no super class.
  • This allows you to add additional DTO types closely related to the base schema.

Repositories

Repositories define the methods used to access and manipulate data.

In Account.model:

repo {
  dbo.Account_GetById(int id)
    => Account,

  dbo.Account_GetWithPassword(string email)
    => Account.WithPassword,

  dbo.List_WithSubRecords_Json()
    => json List<Account.WithSubRecords>,

  ...
}
  • repo: Indicates a repository definition.
  • dbo.Account_GetById(int id): Defines a method named Account_GetById which accepts an int parameter. The generated function will replace . characters with __; the underlying logic is implemented in SQL Server as a stored procedure.
  • => Account: Specifies the return type of the method (optional).
  • json: Denotes that the stored procedure will return JSON-formatted content (optional).
  • Account.WithPassword: Returns an Account object, extended with the WithPassword partial class.

Stored procedures are executed by the default implementation of IModelDbAdapter.

Services

Services define the interface for business logic.

In Account.model:

service {
  AttemptLogin(string email, string password)
    => Account.WithSession,

  LogOut(),

  ResetPassword(string resetToken, string password),

  ...
}
  • service: Indicates a service definition.
  • AttemptLogin(string email, string password): Defines a method named AttemptLogin that accepts string parameters.
  • => Account.WithSession: Specifies the return type of the method (optional).

A DI-injectable interface named Account.IService is generated for both backend and frontend C# code. An interface is also generated called Account.IBackendService, which you must fully implement as a service class, then register for DI in the backend Program.cs.

In Program.cs:

builder.Services.AddAccountBackend<MyAccountService>(); // Use backend services

// - or -

builder.Services.AddAccountFrontend(); // Use HTTP client services

In MyComponent.razor or MyBackendService.cs:

@inject Generated.Account.IService accountService

// - or -

public class MyBackendService(Generated.Account.IService accountService)
{
    // ...
}

When service functions are called from the backend, the service implementation provided for Account.IBackendService will be called wrapped by the default implementation of IModelDbWrapper.

When called from the frontend (e.g., from Blazor WASM), the call is dispatched via a generated HTTP client (through the default implementation of IModelApiAdapter) to a generated API controller, which in turn invokes the backend service implementation (at which point the call is also wrapped by IModelDbWrapper).

Reactive Components

Reactive components define reusable UI elements. These are separated into state, actions, and body definitions.

  • state { ... }: Defines the component's state variables.
state {
  int count,
  bool enabled = {true},
  string firstName = {""},
  ...
}
  • interface { ... }: Defines the actions the component can perform.
interface {
  Increment(),
  Decrement(),
  Add(int amount),
  ...
}
  • Body Definition: Defines the HTML structure of the component. Uses a fully parenthesized prefix notation, which allows attribute and parameter definitions for HTML elements and sub-components.
div(| class = {"d-flex flex-row gap-2"} |
  div(| class = {"my-auto"} |
    {"Count:" + count}
  )
  button(|
    class = {"btn btn-danger"},
    onclick = {"dispatch(this, 'Decrement');"}
    |
    <">-</">
    )
  button(|
    class = {"btn btn-success"},
    onclick = {"dispatch(this, 'Increment');"}
    |
    <">+</">
  )
)
  • The | characters are used to enclose attribute definitions within HTML tags and parameter lists for sub-components.
  • HTML attribute and sub-component parameter value expressions are wrapped with { and }.
  • HTML attribute and sub-component parameters are C# expressions which can reference state.
  • dispatch(this, 'action', pars) is a JS function used to trigger actions within the component, supporting passing action parameters as a JSON object.
  • Regular HTML elements are denoted using identifiers in lowercase, such as button and input.
  • Sub-components, defined in .model files, are denoted using capitalized names such as MyComponent.
  • If your component has no interface entries, a default implementation named MyComponent.Default; register this as a DI-injectable service:
builder.Services.AddMyComponentView<MyComponentBase.Default>();
  • If your component defines interface entries, fully implement the MyComponentBase abstract class as a service class and inject that instead.
builder.Services.AddMyComponentView<MyComponentImpl>();

More elaborate syntax and control flows are available, including conditionals and loops. Refer to the specific documentation for using flow-control and understanding how the DOM syntax tree is assembled.

Clone this wiki locally