Skip to content

Model-bound queries

When a screen needs a list of accounts, you should not need a controller just to forward a database call. Put a static method on the returned model and mark the model with [ReadModel]. Arc discovers the query and generates its HTTP endpoint and TypeScript proxy.

A read model is the shape you serve. It can come from MongoDB, EF Core, a service, or in-memory data. Neither [ReadModel] nor a database query requires Chronicle or event sourcing.

Cratis applications conventionally give domain values their own types. AccountId says which kind of identity a method accepts; AccountName is not interchangeable with an address or another string. The C# compiler can catch accidental swaps before the application runs. Define shared concepts once in the feature and reuse them in its commands and read models:

using System;
using Cratis.Concepts;
namespace Banking.Accounts;
public record AccountId(Guid Value) : ConceptAs<Guid>(Value)
{
public static readonly AccountId NotSet = new(Guid.Empty);
public static AccountId New() => new(Guid.NewGuid());
public static implicit operator AccountId(Guid value) => new(value);
}
public record AccountName(string Value) : ConceptAs<string>(Value)
{
public static readonly AccountName NotSet = new(string.Empty);
public static implicit operator AccountName(string value) => new(value);
}

These are standalone domain concepts, not Chronicle event-source identities. Arc and its MongoDB integration handle their underlying wire/storage values, and the proxy generator maps them to the corresponding TypeScript value types. The C# domain distinctions are not automatically branded TypeScript types.

This is a modeling convention, not a requirement imposed by [ReadModel]. Add a ConceptValidator<T> when an invariant should follow a value wherever Arc validates it. See concepts and serialization and the validation tutorial.

Use the concepts above in an Arc host with the MongoDB provider configured and an IMongoCollection<DebitAccount> available from DI. The following is the complete read-model declaration, not a complete host. Later pages show alternative declarations of this same model; do not add all of them as duplicate types.

using System.Collections.Generic;
using Cratis.Arc.Queries.ModelBound;
using MongoDB.Driver;
namespace Banking.Accounts;
[ReadModel]
public record DebitAccount(AccountId Id, AccountName Name, decimal Balance)
{
[Path("/api/accounts")]
public static IEnumerable<DebitAccount> AllAccounts(IMongoCollection<DebitAccount> collection) =>
collection.Find(_ => true).ToList();
}

GET /api/accounts now returns a query result envelope whose data is the account list. The explicit path makes this example independent of namespace-routing configuration. AllAccounts becomes the generated query class name; its fully qualified query name is Banking.Accounts.DebitAccount.AllAccounts.

This unprotected teaching example is suitable only for public data or a local sandbox. Before exposing account data, add authorization.

Use a non-generic static method returning the declaring read-model type, a collection of that type, or one of its supported wrappers. Public methods are the recommended declaration style.

For example, a method on DebitAccount returning IEnumerable<DebitAccount> qualifies. A method there returning AccountSummary, int, or PagedResult<DebitAccount> does not. Put a summary query on an [ReadModel] AccountSummary instead, or choose a controller. Compiling a method is not proof that Arc discovers an endpoint for it.

Task<T> can wrap a supported result, including ISubject<T>. Query dependencies are method parameters resolved by type; caller arguments are the other parameters. See static methods and dependency injection.

  1. Add scalar arguments and validation.
  2. Protect the query with Arc authentication and role checks; do not assume ASP.NET policies run in the model-bound evaluator.
  3. Return IQueryable<DebitAccount> for paging and sorting.
  4. Return a supported stream for observable updates.
  5. Consume the generated proxy in React queries.