jaunty Architecture › Metadata System Specification

Metadata System Specification

Version: 2026.02.19
Status: Active

The metadata system is the foundation of Jaunty's performance. It builds and caches entity metadata once per type, eliminating reflection at query execution time.


Overview

Usage Phase (Per-query)

GetSetters(reader)

Match columns to properties

Return PropertySetter[]

Execute setters (zero reflection)

Cache Phase (One-time)

MetadataCache static ctor

Create PropertyContext

Compile expression trees

Build ColumnToIndex map

Build Phase (One-time)

MetadataBuilder.Build()

Extract attributes
[Table], [Column], [Key]

Apply JauntyConfig
resolvers

Build EntityMetadata

Key Characteristics:

  • Build Phase: Runs once per type, uses reflection
  • Cache Phase: Compiles delegates, stores in static fields
  • Usage Phase: Zero reflection, O(1) lookups

Component Architecture

System Component Diagram

uses

contains

contains

creates

checks first

«static»

MetadataCache_T

+EntityMetadata Metadata

-Properties PropertyContext[]

-ColumnToIndex Dictionary/FrozenDictionary

+GetSetters(reader, mode) : PropertySetter[]

+CreateSetter(property) : Action

«static»

MetadataBuilder

+Build() : EntityMetadata

-ResolveColumn(property) : string

EntityMetadata

+TableName string

+SchemaName string

+Columns ColumnMetadata[]

+KeyProperty ColumnMetadata

+NonKeyProperties ColumnMetadata[]

ColumnMetadata

+Property PropertyInfo

+ColumnName string

+IsKey bool

+IsIdentity bool

PropertyContext_T

+Property PropertyInfo

+Setter Action

+PropertyName string

+ColumnName string

+IsNonNullable bool

PropertySetter_T

+Set(target, record) : void

«static»

MappedCache_T

+Mapper Func


Initialization Flow

Static Constructor Sequence

ExpressionTreesMetadataBuilderMetadataCacheFirstQueryCLRExpressionTreesMetadataBuilderMetadataCacheFirstQueryCLRloop[For each column]All subsequent queriesuse cached metadata (no reflection)Access MetadataCache<T>Run static constructorBuild<T>()Get [Table] attributeGet [Column] attributesApply JauntyConfig resolversEntityMetadataExtract columnsCreateSetter(property)Build expression treeCompile()Action<T, IDataRecord, int>Create PropertyContext<T>Add to ColumnToIndexCreate FrozenDictionary (net8.0)Initialization completeReturn cached metadata

Metadata Resolution

Column Name Resolution Priority

Yes

No

Yes

No

Property 'ProductName'

Has [Column]
attribute?

Use [Column] name
e.g., 'prod_name'

JauntyConfig.
ColumnNameResolver?

Use resolver result
e.g., 'product_name'

Use property name
'ProductName'

Final column name

Example:

C#
public class Product
{
    // [Column] takes priority
    [Column("prod_id")]
    public int Id { get; set; }
    
    // JauntyConfig resolver applies (if no [Column])
    public string ProductName { get; set; }
    
    // JauntyConfig resolver applies
    public decimal Price { get; set; }
}

// With JauntyConfig.ColumnNameResolver set to a snake_case helper you supply
// Resolution:
// Id -> "prod_id" (from [Column])
// ProductName -> "product_name" (from resolver)
// Price -> "price" (from resolver)

Expression Tree Compilation

CreateSetter Process

Yes

No

CreateSetter(property)

Create parameters

target: Parameter(typeof(T))

record: Parameter(typeof(IDataRecord))

index: Parameter(typeof(int))

Call record.GetValue(index)

Get property type

Nullable type?

Convert.ChangeType
to underlying type

Convert.ChangeType
to property type

Wrap in Nullable

Expression.Assign
target.Property = value

Expression.Lambda>

Compile()

Return compiled delegate

Generated Expression Tree

C#
// For property: public int Id { get; set; }

// Generated lambda:
(T target, IDataRecord record, int index) =>
{
    target.Id = (int)Convert.ChangeType(
        record.GetValue(index),
        typeof(int)
    );
};

// Compiled to IL (no reflection at runtime):
IL_0000: ldarg.0  // target
IL_0001: ldarg.1  // record
IL_0002: ldarg.2  // index
IL_0003: callvirt IDataRecord.GetValue
IL_0008: ldclass typeof(int)
IL_000d: call Convert.ChangeType
IL_0012: unbox.any int
IL_0017: stfld Product.Id

Getter/Setter Resolution

GetSetters Flow

Found

Not Found

Yes

No

Yes

No

Yes

No

Yes

No

Yes

No

No

Yes

GetSetters(reader, mode)

reader.FieldCount

new PropertySetter[fieldCount]

Span matchedProperties

For i = 0 to fieldCount-1

reader.GetName(i)

ColumnToIndex.
TryGetValue(name)

Already
matched?

mode ==
Strict?

mode ==
Strict?

Create PropertySetter

Throw InvalidOperationException
'Property mapped more than once'

More columns?

Throw InvalidOperationException
'Column has no mapping'

matchedProperties[index] = true

mode == Strict?

All properties
matched?

Return PropertySetter[]

Throw InvalidOperationException
'Property missing from result'


NULL Handling

NULL Resolution Flow

No

Yes

Yes

No

PropertySetter.Set(target, record)

record.
IsDBNull(ordinal)?

context.Setter(target, record, ordinal)

IsNonNullable
type?

Throw InvalidOperationException
'Cannot assign NULL to
non-nullable property'

Skip (keeps default value)

Property set

Property = default

Exception thrown

Type Handling:

Type NULL Behavior
string Becomes null
int? Becomes null
DateTime? Becomes null
int Throws InvalidOperationException
DateTime Throws InvalidOperationException
bool Throws InvalidOperationException

Configuration Interaction

Static Caching Behavior

MetadataCacheJauntyConfigAppMetadataCacheJauntyConfigAppApplication StartupFirst Query (triggers caching)Configuration Change (INEFFECTIVE)Second Query (uses cached metadata)Configuration changes after firstuse have NO EFFECT on cached typesColumnNameResolver = ToSnakeCaseTableNameResolver = SnakeCasePluralQuery<Product>(sql)Static constructor runsRead resolversCache with snake_case namesColumnNameResolver = nullQuery<Product>(sql)Use CACHED metadataStill uses snake_case

Performance Characteristics

Initialization Cost (One-Time per Type)

000ms000ms000ms000ms000ms000ms000msMetadataBuilder.Build<T> Extract attributes Apply resolvers Create PropertyContext Build expression trees Compile delegates Build ColumnToIndex Ready for queries BuildCacheCompleteMetadataCache Initialization Timeline

Typical Cost:

  • Entity with 10 properties: ~1-5ms
  • Entity with 50 properties: ~5-15ms

Query Execution Cost (Per-Query)

Per-Query Overhead by Entity SizeMetadata lookupColumnToIndex lookupProperty setters5102050100Properties109876543210Time per row (μs)

Typical Cost:

  • Metadata lookup: O(1) - ~10ns
  • Column-to-index lookup: O(1) - ~50ns (FrozenDictionary)
  • Property setter invocation: O(1) - ~100ns per property

Thread Safety

Static Initialization Guarantee

Thread3Thread2Thread1CLRThread3Thread2Thread1CLRAll threads now havelock-free read accessAccess MetadataCache<T>Check if initializedRun static constructorAccess MetadataCache<T>Wait (constructor in progress)Access MetadataCache<T>Wait (constructor in progress)Build metadataCompile delegatesConstructor completeReturn initialized cacheReturn initialized cache

Guarantees:

  • Static constructors are thread-safe by CLR guarantee
  • Only one thread runs the constructor
  • Other threads wait until initialization completes
  • After initialization: lock-free reads (all fields are readonly)

Extension Points

IMapped Override

Yes

No

User implements IMapped

Define static Map method

MappedCache.Mapper set

DrDispatcher.Resolve()

MappedCache.Mapper
not null?

Use custom Map method

Use MetadataCache

Skip metadata reflection
Skip expression compilation

Normal metadata path

Example:

C#
public class Product : IMapped<Product>
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    
    // Custom mapping - bypasses MetadataCache
    public static void Map(ref Product target, IDataReader reader, int columnIndex)
    {
        target.Id = reader.GetInt32(columnIndex);
        target.Name = reader.GetString(columnIndex + 1);
        target.Price = reader.GetDecimal(columnIndex + 2);
    }
}

Error Handling

Error Scenarios

Yes

No

GetSetters execution

Error type?

Empty result set

Duplicate column mapping

Column has no property mapping

Property missing from result

NULL for non-nullable property

Return empty array

mode == Strict?

Throw InvalidOperationException

Throw InvalidOperationException
with detailed message

Ignore (partial mode)

Error Message Examples

text
┌─────────────────────────────────────────────────────────────────┐
│ Strict mapping failed: property 'Price' (mapped to column      │
│ 'price') was missing from the result set.                       │
│                                                                 │
│ Type: MyApp.Product                                             │
│ SQL columns: [id, name, category_id]                           │
│ Missing properties: [Price]                                     │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ Strict mapping failed: Property 'Name' was mapped more than    │
│ once from the result set.                                       │
│                                                                 │
│ Type: MyApp.Product                                             │
│ Duplicate column: 'name' appears at positions 1 and 3          │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ Cannot assign NULL to non-nullable property 'Id' on type       │
│ 'Product'.                                                      │
│                                                                 │
│ Column: 'id' (ordinal: 0)                                       │
│ Value: DBNull                                                   │
└─────────────────────────────────────────────────────────────────┘

See Also

Document Purpose
architecture-specification.md Full architecture
parameter-binding-spec.md Parameter binding
performance-spec.md Performance optimization
../../01-api-reference/attributes.md Mapping attributes
../../01-api-reference/configuration.md JauntyConfig