Skip to main content

Overall architecture

This page is the map you keep coming back to. It doesn't go deep on any one subsystem — each "Creating a..." guide and the Reference section do that — but it should let you place any file you're looking at into the right layer.

Your configuration's own layout

This is what dotnet new kandra-config -n Acme puts in your repo — 11 projects, all prefixed with your configuration's name, nothing else:

Acme.slnx
src/
Acme.Domain # concrete *Base entities (Entities/Dictionaries, /Documents, /Registers, ...)
Acme.Forms # concrete *Dtos + *Validators (client+server shared)
Acme.Application # concrete *Behaviors + AutoMapper profiles, generated DI wiring
Acme.Persistence # generated EntityConfigurations, repositories
Acme.Persistence.Databases # DbContext(s) + per-provider Migrations (only for providers you selected)
Acme.WebApi # runnable host — generated controllers land here
Acme.Client.MudBlazor # the runnable Blazor WebAssembly client — 100% generated pages
Acme.ClientLib.Common # hand-written Refit interfaces + client services
Acme.Enums
Acme.Localization
Acme.Tests

It starts empty (no Dictionaries/Documents/Reports/Registers/Data Processors) and grows one entity at a time via the shipped .claude/skills/ — see How to start.

The one rule that matters most inside this shape: Acme.Forms must never reference Acme.Application/Acme.Domain/Acme.Persistence. The Blazor WebAssembly client only ever sees Forms (plus Enums/Localization); the server references Forms too, because mappers and behaviors need both the *Base entity and the *Dto. Cross that boundary and the client build breaks trying to pull in EF Core.

What you're referencing, not browsing

Kandra.* is consumed purely via NuGet PackageReference — there is no platform source anywhere in your repo, unless you've separately checked out the platform team's own monorepo for reference (see How to start). The package names below tell you where a given concept's logic actually lives, even though you can't cd into it:

PackageWhat it's responsible for
Kandra.DomainEntity base classes, register engine, Identity contracts — EF-free
Kandra.Application(.Abstractions)CRUD services, behavior/validator/mapper contracts, numbering
Kandra.Persistence(.Abstractions)EF Core integration, register writers/readers, accounting subsystem
Kandra.Api.LibASP.NET Core wiring (AddKandra/UseKandra), controller base classes
Kandra.AttributesThe metadata vocabulary the generators read
Kandra.ClientLib.Common/.MudBlazorClient-side service/DI plumbing
Kandra.Generators.*Roslyn source generators (run at your build time, from the NuGet package)
Kandra.PrintingPDF/CSV export, timezone-aware value formatting

$(KandraCoreVersion) in your Directory.Build.props pins which published build of these packages you're on — a manually-maintained version string, bumped by hand after you've validated a newer engine release actually works with your configuration.

The generation pipeline, end to end

You write: *Base entity + *Dto (attributed) + *Behavior + *Validator


Kandra.Generators.* read the Dto/entity attributes at compile time

┌───────────┼──────────────────┬────────────────────┬──────────────┐
▼ ▼ ▼ ▼ ▼
EF Core CRUD controller Client DI (Refit DI registration Generated Blazor
config (Kandra.Generators. client + service blocks UI pages
(Generated Api) → WebApi wrapper) → (ConfigureServices (Document/Report
.Net/) ClientLib .cs) emitters)

Nothing here is a runtime reflection pass — it all happens at dotnet build time, and the output is ordinary checked-in-shape C#/Razor under each project's Generated.Net/ folder. Read it when you want to know exactly what an attribute produces; never hand-edit it.

Request-time flow (a document submit, as the representative case)

  1. Blazor client posts to a generated controller endpoint.
  2. The generated controller calls into the application-layer CRUD service, which resolves the entity's *Behavior.
  3. OnBeforeSaveAsync / OnSaveAsync run around the EF Core save.
  4. On submit specifically, OnSubmitAsync runs with an ISubmitScope parameter — the only legal path to ISubmitScope.GetWriter<TWriter>() (register writers) and IPostingService (chart-of-accounts postings), both keyed DI services unreachable by ordinary constructor injection. This is a deliberate chokepoint: nothing outside a submit can write a register or post a transaction.
  5. Time zone conversion happens on the way out: the server stores/emits UTC everywhere; a registered JsonConverter<DateTime> converts every DTO field crossing the wire for display, and Kandra.Printing's PrintValueFormatter converts server-side for PDF/CSV export. The client detects its IANA time zone natively and sends it as an X-Time-Zone header on every request.

Two accounting-adjacent subsystems that are deliberately not unified

  • Register engine (Kandra.Domain/Kandra.Persistence registers) — general analytical storage: balances, turnovers, info records. Any document can write any register it needs.
  • Chart-of-accounts / posting subsystem (Kandra.Persistence/Accounting, Acc_ tables) — a purpose-built double-entry-style accounting engine with its own account codes, subconto dimension slots, and posting batches.

They look similar (both are "documents write structured records on submit") but are built independently on purpose — see Register Engine and Chart of Accounts / Posting for why, and don't try to implement one in terms of the other.

Identity and localization, briefly

  • Identity is generic over <TUser, TRole> at the platform layer, closed to concrete types per configuration. Two JWT paths: human users, and API keys for non-human callers.
  • Localization is a stacked-module chain (LocalizationChain) — the most-recently-registered module wins per key, falling through to earlier modules. services.AddLocalization() must be called before AddLocalizationModule().

See Identity & Auth and Localization for the full picture of each.

Where to go next

Pick the entity kind you're about to build: Dictionary, Register, Document, Report, Data processor, or Constant — each guide assumes you've read this page.