Skip to content

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.

  • DirectoryGranit.ReferenceData/ i18n entity, store abstractions, registry, fluent builder
    • Granit.ReferenceData.EntityFrameworkCore EF Core store, seeding, QueryDefinition
    • Granit.ReferenceData.Endpoints Generic + dynamic CRUD endpoints
PackageRoleDepends on
Granit.ReferenceDataIReferenceDataStoreReader/Writer<T>, ReferenceDataEntity, ReferenceDataRegistryGranit, Granit.QueryEngine
Granit.ReferenceData.EntityFrameworkCoreEF Core store, seeding, dynamic column mapping, QueryDefinitionGranit.ReferenceData, Granit.Persistence, Granit.Caching
Granit.ReferenceData.EndpointsGeneric + dynamic CRUD endpoints, QueryEngine-powered listing, hierarchical childrenGranit.ReferenceData, Granit.QueryEngine.AspNetCore, Granit.Validation, Granit.Guids
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).

Granit.ReferenceData supports two registration approaches:

For reference data types with domain-specific properties as CLR fields. Full compile-time type safety.

// 1. Define entity
public sealed class Country : ReferenceDataEntity
{
public string Alpha3Code { get; set; } = string.Empty;
}
// 2. Configure EF Core
public 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 + endpoints
services.AddReferenceDataStore<Country, AppDbContext>();
app.MapGranitReferenceData<Country>();

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 properties
country.SetMetadataValue("Continent", "Europe");
country.SetMetadataValue("Population", "11500000");
// Read extra properties
string? continent = country.GetMetadataValue("Continent");
int? population = country.GetMetadataValue<int>("Population");
bool hasContinent = country.HasMetadataValue("Continent");
// Remove an extra property
country.SetMetadataValue("Continent", null);

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));
});

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:

MethodRouteDescription
GET/{prefix}/{entities}/{code}/childrenDirect children of a parent entry
// In code, use the reader
IReadOnlyList<Category> children = await storeReader
.GetChildrenAsync("ELECTRONICS", cancellationToken);
// Get root entries (no parent)
IReadOnlyList<Category> roots = await storeReader
.GetChildrenAsync(null, cancellationToken);
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);
}

Read (requires ReferenceData.Entries.Read)

Section titled “Read (requires ReferenceData.Entries.Read)”
MethodRouteDescription
GET/{prefix}/{entities}QueryEngine-powered paginated list
GET/{prefix}/{entities}/metaQuery metadata (columns, filters, sorts)
GET/{prefix}/{entities}/{code}Single entry by code
GET/{prefix}/{entities}/{code}/childrenDirect 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)”
MethodRoutePermissionDescription
POST/{prefix}/{entities}Entries.CreateCreate an entry
PUT/{prefix}/{entities}/{code}Entries.ManageUpdate an entry
DELETE/{prefix}/{entities}/{code}Entries.ManageDeactivate (soft delete)

Create and update requests accept Metadata (max 50 entries) and ParentCode.

PermissionScope
ReferenceData.Entries.ReadQuery and retrieve entries
ReferenceData.Entries.CreateCreate new entries
ReferenceData.Entries.ManageUpdate and deactivate entries

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).

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>() with isFilterable / isSortable
  • Global search on Code, LabelEn, LabelFr
  • Default sort by SortOrder, then Code

Shadow properties use EF.Property<T>() expressions for SQL-level filtering and sorting — the FilterExpressionBuilder and QueryableSortExtensions handle them transparently.

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);
}
}
}
CategoryKey typesPackage
EntityReferenceDataEntity, DynamicReferenceDataEntity, IHasMetadataGranit.ReferenceData, Granit
StoreIReferenceDataStoreReader<T>, IReferenceDataStoreWriter<T>, IReferenceDataSeeder<T>Granit.ReferenceData
RegistrationAddReferenceDataStore<T, TDb>(), AddReferenceData<TDb>(), ReferenceDataBuilderGranit.ReferenceData.EntityFrameworkCore
EndpointsMapGranitReferenceData<T>(), MapGranitAllReferenceData()Granit.ReferenceData.Endpoints
RegistryReferenceDataRegistry, ReferenceDataTypeRegistrationGranit.ReferenceData
MetadataMetadataExtensions, MetadataMappingOptions<T>Granit, Granit.Persistence