Reference Data — Lookup Tables & Enums
ReferenceDataEntity is an abstract base class for i18n lookup tables (countries,
currencies, document types). Each entity has a unique Code, 14 language labels,
automatic label resolution based on CurrentUICulture, and an extensible
Metadata JSON bag for application-specific fields.
Package structure
Section titled “Package structure”DirectoryGranit.ReferenceData/ i18n entity, store abstractions, registry, fluent builder
- Granit.ReferenceData.EntityFrameworkCore EF Core store, seeding, QueryDefinition
- Granit.ReferenceData.Endpoints Generic + dynamic CRUD endpoints
| Package | Role | Depends on |
|---|---|---|
Granit.ReferenceData | IReferenceDataStoreReader/Writer<T>, ReferenceDataEntity, ReferenceDataRegistry | Granit, Granit.QueryEngine |
Granit.ReferenceData.EntityFrameworkCore | EF Core store, seeding, dynamic column mapping, QueryDefinition | Granit.ReferenceData, Granit.Persistence, Granit.Caching |
Granit.ReferenceData.Endpoints | Generic + dynamic CRUD endpoints, QueryEngine-powered listing, hierarchical children | Granit.ReferenceData, Granit.QueryEngine.AspNetCore, Granit.Validation, Granit.Guids |
Entity structure
Section titled “Entity structure”public abstract class ReferenceDataEntity : AuditedEntity, IActive, IHasMetadata, IEmitEntityLifecycleEvents{ public string Code { get; set; }
// 14 language labels public string LabelEn { get; set; } public string LabelFr { get; set; } // ... LabelNl, LabelDe, LabelEs, LabelIt, LabelPt, // LabelZh, LabelJa, LabelPl, LabelTr, LabelKo, LabelSv, LabelCs
// Resolved at runtime via CurrentUICulture (not mapped to DB) public virtual string Label { get; }
public bool Activated { get; set; } public int SortOrder { get; set; } public DateTimeOffset? ValidFrom { get; set; } public DateTimeOffset? ValidTo { get; set; }
// Metadata — JSON property bag for application-level extensibility public string? MetadataJson { get; set; }
// Optional parent code for hierarchical reference data public string? ParentCode { get; set; }}The Label property resolves the appropriate label based on
CultureInfo.CurrentUICulture.TwoLetterISOLanguageName, falling back to LabelEn when
the translation is empty. Regional variants use the base language fallback
(fr-CA uses LabelFr, en-GB uses LabelEn, pt-BR uses LabelPt).
Two registration modes
Section titled “Two registration modes”Granit.ReferenceData supports two registration approaches:
For reference data types with domain-specific properties as CLR fields. Full compile-time type safety.
// 1. Define entitypublic sealed class Country : ReferenceDataEntity{ public string Alpha3Code { get; set; } = string.Empty;}
// 2. Configure EF Corepublic sealed class CountryConfiguration : ReferenceDataEntityTypeConfiguration<Country>{ public CountryConfiguration() : base("ref_countries") { }
protected override void ConfigureEntity(EntityTypeBuilder<Country> builder) { builder.Property(e => e.Alpha3Code).HasMaxLength(3).IsRequired(); }}
// 3. Register store + endpointsservices.AddReferenceDataStore<Country, AppDbContext>();app.MapGranitReferenceData<Country>();For simple reference data types that only need custom properties without
creating a C# class. Properties are stored in the JSON bag or promoted to
real SQL columns via MapProperty<T>().
// 1. Register in one fluent call — no entity subclass neededservices.AddReferenceData<AppDbContext>(rd =>{ rd.Add("Countries", opts => opts .Table("ref_countries") .MapProperty<string>("Alpha3Code", maxLength: 3, isFilterable: true) .MapProperty<string>("Region", maxLength: 100, isFilterable: true) .MapProperty<bool>("IsEuMember", isFilterable: true));
rd.Add("DocumentTypes", opts => opts.Table("ref_document_types"));
rd.Add("Categories", opts => opts .Table("ref_categories") .Hierarchical());});
// 2. Map all registered types in one callapp.MapGranitAllReferenceData();Metadata — application-level extensibility
Section titled “Metadata — application-level extensibility”Every ReferenceDataEntity implements IHasMetadata. This provides a
JSON property bag (MetadataJson column) for attaching custom metadata
without schema changes.
// Write extra propertiescountry.SetMetadataValue("Continent", "Europe");country.SetMetadataValue("Population", "11500000");
// Read extra propertiesstring? continent = country.GetMetadataValue("Continent");int? population = country.GetMetadataValue<int>("Population");bool hasContinent = country.HasMetadataValue("Continent");
// Remove an extra propertycountry.SetMetadataValue("Continent", null);Promoting properties to SQL columns
Section titled “Promoting properties to SQL columns”Properties that need SQL indexes or query filtering can be promoted to real
columns via MapProperty<T>(). These become EF Core Shadow Properties —
indexable and queryable, while remaining accessible through GetMetadataValue().
services.AddReferenceData<AppDbContext>(rd =>{ rd.Add("Countries", opts => opts .Table("ref_countries") .MapProperty<string>("Alpha3Code", maxLength: 3, isFilterable: true));});Hierarchical reference data
Section titled “Hierarchical reference data”Reference data types can support parent-child relationships via ParentCode.
Enable hierarchy with .Hierarchical() in the dynamic registration API.
rd.Add("Categories", opts => opts .Table("ref_categories") .Hierarchical());This exposes an additional endpoint:
| Method | Route | Description |
|---|---|---|
GET | /{prefix}/{entities}/{code}/children | Direct children of a parent entry |
// In code, use the readerIReadOnlyList<Category> children = await storeReader .GetChildrenAsync("ELECTRONICS", cancellationToken);
// Get root entries (no parent)IReadOnlyList<Category> roots = await storeReader .GetChildrenAsync(null, cancellationToken);Store abstractions
Section titled “Store abstractions”public interface IReferenceDataStoreReader<TEntity> where TEntity : ReferenceDataEntity{ Task<PagedResult<TEntity>> GetAllAsync( ReferenceDataQuery? query = null, CancellationToken cancellationToken = default);
Task<TEntity?> GetByCodeAsync( string code, CancellationToken cancellationToken = default);
Task<IReadOnlyList<TEntity>> GetChildrenAsync( string? parentCode, CancellationToken cancellationToken = default);}
public interface IReferenceDataStoreWriter<in TEntity> where TEntity : ReferenceDataEntity{ Task CreateAsync(TEntity entity, CancellationToken cancellationToken = default); Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default); Task SetActiveAsync(string code, bool isActive, CancellationToken cancellationToken = default);}Endpoints
Section titled “Endpoints”Read (requires ReferenceData.Entries.Read)
Section titled “Read (requires ReferenceData.Entries.Read)”| Method | Route | Description |
|---|---|---|
GET | /{prefix}/{entities} | QueryEngine-powered paginated list |
GET | /{prefix}/{entities}/meta | Query metadata (columns, filters, sorts) |
GET | /{prefix}/{entities}/{code} | Single entry by code |
GET | /{prefix}/{entities}/{code}/children | Direct children (hierarchical types) |
The list endpoint (GET /) uses the Granit QueryEngine. Query parameters follow the
standard QueryEngine syntax: filter[code.contains]=BE, sort=LabelEn,
search=belg, page=1, pageSize=20. See QueryEngine
for full syntax.
The /meta endpoint returns column definitions, available filters, and sort options
for building dynamic query UIs. Disable with IncludeMetaEndpoint = false.
Admin (requires ReferenceData.Entries.Create / ReferenceData.Entries.Manage)
Section titled “Admin (requires ReferenceData.Entries.Create / ReferenceData.Entries.Manage)”| Method | Route | Permission | Description |
|---|---|---|---|
POST | /{prefix}/{entities} | Entries.Create | Create an entry |
PUT | /{prefix}/{entities}/{code} | Entries.Manage | Update an entry |
DELETE | /{prefix}/{entities}/{code} | Entries.Manage | Deactivate (soft delete) |
Create and update requests accept Metadata (max 50 entries) and ParentCode.
Permissions
Section titled “Permissions”| Permission | Scope |
|---|---|
ReferenceData.Entries.Read | Query and retrieve entries |
ReferenceData.Entries.Create | Create new entries |
ReferenceData.Entries.Manage | Update and deactivate entries |
Validation
Section titled “Validation”The Code field must match ^[A-Za-z0-9_.\-]+$ (letters, digits, hyphens,
underscores, dots). Maximum 50 characters. Metadata is limited to 50
key-value pairs (keys max 50 chars, values max 4 000 chars).
QueryEngine integration
Section titled “QueryEngine integration”Both strongly-typed and dynamic reference data types automatically generate a
QueryDefinition that declares:
- Base columns:
Code,LabelEn,LabelFr,LabelNl,LabelDe,Activated,SortOrder,ValidFrom,ValidTo - Shadow property columns declared via
MapProperty<T>()withisFilterable/isSortable - Global search on
Code,LabelEn,LabelFr - Default sort by
SortOrder, thenCode
Shadow properties use EF.Property<T>() expressions for SQL-level filtering and
sorting — the FilterExpressionBuilder and QueryableSortExtensions handle them
transparently.
Seeding reference data
Section titled “Seeding reference data”Implement IReferenceDataSeeder<TEntity> and register it. The IDataSeedContributor
bridge invokes seeders during application startup.
public sealed class CountrySeeder : IReferenceDataSeeder<Country>{ public int Order => 1;
public async Task SeedAsync( IReferenceDataStoreReader<Country> storeReader, IReferenceDataStoreWriter<Country> storeWriter, CancellationToken cancellationToken) { if (await storeReader.GetByCodeAsync("BE", cancellationToken) is null) { await storeWriter.CreateAsync(new Country { Code = "BE", LabelEn = "Belgium", LabelFr = "Belgique", LabelNl = "België", Alpha3Code = "BEL", SortOrder = 1 }, cancellationToken); } }}Public API summary
Section titled “Public API summary”| Category | Key types | Package |
|---|---|---|
| Entity | ReferenceDataEntity, DynamicReferenceDataEntity, IHasMetadata | Granit.ReferenceData, Granit |
| Store | IReferenceDataStoreReader<T>, IReferenceDataStoreWriter<T>, IReferenceDataSeeder<T> | Granit.ReferenceData |
| Registration | AddReferenceDataStore<T, TDb>(), AddReferenceData<TDb>(), ReferenceDataBuilder | Granit.ReferenceData.EntityFrameworkCore |
| Endpoints | MapGranitReferenceData<T>(), MapGranitAllReferenceData() | Granit.ReferenceData.Endpoints |
| Registry | ReferenceDataRegistry, ReferenceDataTypeRegistration | Granit.ReferenceData |
| Metadata | MetadataExtensions, MetadataMappingOptions<T> | Granit, Granit.Persistence |
See also
Section titled “See also”- Use Reference Data guide — step-by-step walkthrough
- Localization — i18n for UI strings and error messages
- Persistence — isolated DbContext pattern for all EF Core stores
- Metadata — JSON property bag for entity extensibility