Flat dictionary: Branch
Full worked example for the flat shape — see Creating a dictionary for the conceptual
background (the four generator-discovered pieces, when to reach for hierarchical instead) and
Hierarchical dictionary: Counterparty for the other shape. If you have the
create-dictionary skill available, prefer it over hand-rolling these steps — this page is
what its data/Domain.Flat.cs/Forms.Flat.cs/Application.Flat.cs/ClientLib.Flat.cs
templates produce once filled in.
Branch is about as simple as a dictionary gets: no custom fields beyond Code/Name
(inherited from DictionaryBase unmodified), Code auto-numbered BR-000001, BR-000002,
... The real Branch in Kandra's own reference configuration also carries a
[FilePicker]-backed RegistrationDocumentId field with blob reconciliation wired into its
Behavior — that's a separate concern, not core to being a dictionary; see
File storage & attachments if you need it. Left out here to keep this
example focused on the four-piece pattern plus the two hand-written extras.
1. Mint a type identity
A fresh file, never an edit to AcmeTypeIds.cs itself (that file is partial and stays
permanently empty of members — see Creating a dictionary's Gotchas section for why):
// Acme.Enums/AcmeTypeIds/AcmeTypeIds.Branch.cs
namespace Acme.Enums;
public static partial class AcmeTypeIds
{
public const string Branch = "18d2d31e-c312-411b-b9eb-291e10d89fd6";
}
Generate your own fresh GUID for a real dictionary — the value above is Branch's actual,
already-registered identity in Kandra's own reference configuration, reused here only because
it's real, verified-working code; never copy a literal type-identity value into a different
dictionary.
2. Domain entity (*Base)
// Acme.Domain/Entities/Dictionaries/Branch.cs
using Kandra.Attributes.Entities;
using Kandra.Domain.Commons;
using Kandra.Domain.Entities.Dictionaries;
using Acme.Enums;
namespace Acme.Domain.Entities.Dictionaries;
/// <summary>
/// Not ISubconto - not used as a posting analytical dimension in the current chart.
/// Still implements IEntityWithTypeId so its TypeId is discoverable the same way as
/// every other document/dictionary in the domain.
/// </summary>
[KandraDictionaryEntity(TableName = "Dict_Branches")]
[KandraGeneratedEquality]
public partial class Branch : DictionaryBase, IEntityWithTypeId
{
public static Guid TypeId { get; } = new(AcmeTypeIds.Branch);
}
[KandraGeneratedEquality] fills in Equals/GetHashCode/ToString over this class's own
declared properties — requires partial. Zero UI/auth attributes on this class, by design: a
background job or importer can reference Acme.Domain without dragging in the web/UI layer.
3. Dto + Validator (*Dto, *Validator)
// Acme.Forms/Dictionaries/Branch.cs
using Kandra.Attributes.Application;
using Kandra.Attributes.Entities;
using Kandra.Attributes.Naming;
using Kandra.Forms.Dictionaries;
using Kandra.Forms.Querying;
using Kandra.Validators.Dictionaries;
using Acme.Enums;
using Microsoft.Extensions.Localization;
namespace Acme.Forms.Dictionaries;
[TypeId(AcmeTypeIds.Branch)]
[KandraDictionaryForm(Name = "Branches", ValidatorType = typeof(BranchValidator), QueryDtoType = typeof(BranchQueryDto))]
[EditFormTitle("EditBranch")]
[AddFormTitle("CreateBranch")]
[ViewFormTitle("ViewBranch")]
[ListFormTitle("Branches")]
[NavigationName("Nav_Branches")]
public class BranchDto : DictionaryDto
{
// Code/Name are inherited unmodified from DictionaryDto — most dictionaries don't need to
// override either. Add fields here as [Required]/[Caption]/[ListFormColumn]-attributed
// properties once this dictionary needs more than Code/Name.
}
public class BranchQueryDto : DictionaryQueryDto
{
}
public class BranchValidator : DictionaryValidator<BranchDto>
{
public BranchValidator(IStringLocalizer localizer) : base(localizer)
{
// base(localizer) already covers Code/Name NotEmpty — add extra RuleFor(...) calls here.
}
}
[KandraDictionaryForm]'s Name ("Branches") drives the generated controller's route
segment and the Refit client's expected [PathPrefix] — it's not derived from the class name,
always set it explicitly and keep step 5 in sync with it.
Every title/nav key referenced above (EditBranch, CreateBranch, ViewBranch, Branches,
Nav_Branches) needs a matching <data name="..."> entry in all three of your
configuration's engine-facing locale files (Acme.Localization/Acme.resx / .ru.resx /
.uk.resx) — a missing key silently renders as the raw key name at runtime, not an error. This
is the application's own runtime localization (see Localization), a
separate concern from this documentation site's own EN/UK translation tracking. Example shape:
<!-- Acme.Localization/Acme.resx -->
<data name="Nav_Branches" xml:space="preserve">
<value>Branches</value>
</data>
<data name="CreateBranch" xml:space="preserve">
<value>Create Branch</value>
</data>
4. Numbering bucket + Behavior + Mapper (*Behavior)
// Acme.Domain/Numbering/NumberingBuckets.Branch.cs
namespace Acme.Domain.Numbering;
public static partial class NumberingBuckets
{
public const string Branch = "Branch";
}
// Acme.Application/Dictionaries/Branch.cs
using AutoMapper;
using Kandra.Application.Abstractions.Behaviors;
using Kandra.Application.Abstractions.Services;
using Kandra.Application.Validation;
using Kandra.Attributes.Application;
using Acme.Domain.Entities.Dictionaries;
using Acme.Domain.Numbering;
using Acme.Forms.Dictionaries;
namespace Acme.Application.Dictionaries;
[KandraDictionaryBehavior(typeof(BranchDto))]
public class BranchBehavior(ISimpleNumberingSystem numbering) : IFlatDictionaryBehavior<Branch>
{
public async ValueTask OnNewAsync(Branch entity, CancellationToken cancellationToken) =>
entity.Code = await numbering.GetNextAsync(NumberingBuckets.Branch, raw => $"BR-{raw:D6}",
requestorTag: "Branch:OnNew", cancellationToken: cancellationToken);
}
public class BranchMapper : Profile
{
public BranchMapper() => CreateMap<Branch, BranchDto>().ReverseMap();
}
public class BranchQueryDtoValidator : PagedQueryDtoValidator<BranchQueryDto>;
A no-op Behavior (nothing overridden beyond the mandatory numbering) is completely valid for
most dictionaries — but the class and its [KandraDictionaryBehavior] attribute must still
exist. The engine fails DI validation at startup without one per dictionary. If Code is
hand-typed instead of auto-numbered (a rare case — Kandra's own Currency dictionary does
this), skip the numbering bucket file entirely and make OnNewAsync a plain no-op.
5. Refit client interface (hand-written)
// Acme.ClientLib.Common/ApiClients/IBranchesApiClient.cs
using Kandra.ClientLib.Common.ApiClients;
using Kandra.Forms.Paging;
using Acme.Forms.Dictionaries;
using Refit;
namespace Acme.ClientLib.Common.ApiClients;
[PathPrefix("/api/v1/dictionaries/Branches")]
public interface IBranchesApiClient : IDictionaryApiClient<BranchDto, PagedResult<BranchDto>, BranchQueryDto>
{
// Nothing to add for the common case — IDictionaryApiClient<> already supplies the full CRUD
// set (AllAsync/QueryAsync/ReadAsync/InsertAsync/UpdateAsync/DeleteAsync/GetChangeStateAsync)
// plus NewAsync/LookupAsync. Refit's own source generator implements the body at compile time
// from these attributes — you only ever write the declaration.
}
[PathPrefix] must match [KandraDictionaryForm(Name = "...")] from step 3 exactly — here,
"Branches" both times.
6. EF migration
cd src/Acme.Persistence.Databases
dotnet kandra-migrate add AddBranch
(or the per-provider dotnet ef migrations add fallback — see
Database migrations if kandra-migrate isn't installed yet in your setup.)
7. Verify
dotnet build Acme.slnx — confirm BranchesController.g.cs now exists under
Acme.WebApi/Generated.Net/. dotnet test Acme.slnx, then run the app and exercise
create/edit/list/delete for Branches in the browser. There's still one more hand-written piece
this walkthrough doesn't cover — the Blazor UI pages themselves (no generator exists for this
yet); copy the closest existing dictionary's Pages/Dictionary/*/*.razor set as a starting
point and wire it to IBranchesApiClient from step 5.
See also
- Hierarchical dictionary: Counterparty — the same pattern with a folder/leaf tree instead of a single class.
- Creating a dictionary — the conceptual overview and the Gotchas that apply to both shapes.