This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
EggMapper is a high-performance .NET object-to-object mapping library targeting zero runtime reflection and zero extra allocations. All mapping delegates are compiled as expression trees during MapperConfiguration construction. The goal is to be the fastest runtime mapper — faster than AutoMapper, Mapster, and AgileMapper on every benchmark scenario.
Shorthand slash commands available in this project:
| Skill | Description |
|---|---|
/test [filter] |
Run unit tests, optionally filtered by class/method name |
/bench [filter] |
Run benchmarks and compare EggMapper vs all competitors |
/feat <description> |
Implement a new feature following the correctness→performance loop |
/perf-check |
Verify no regression after code changes |
/ship |
Create a PR with benchmark evidence |
/expr-debug <types> |
Inspect the compiled expression tree for a type pair |
/pr [action] |
List open PRs, check CI, or merge |
# Build
dotnet build --configuration Release
# Run all unit tests
dotnet test --configuration Release
# Run tests for a specific class (by fully qualified name or filter)
dotnet test src/EggMapper.UnitTests/EggMapper.UnitTests.csproj --configuration Release --filter "FullyQualifiedName~BasicFlatteningTests"
# Run all benchmarks on .NET 10 (takes several minutes)
cd src/EggMapper.Benchmarks && dotnet run -c Release -f net10.0 -- --filter * --exporters json markdown
# Run a single benchmark class
cd src/EggMapper.Benchmarks && dotnet run -c Release -f net10.0 -- --filter *FlatMappingBenchmark*
# Quick smoke-test benchmark (short runs, not accurate for perf comparison)
cd src/EggMapper.Benchmarks && dotnet run -c Release -f net10.0 -- --filter *FlatMappingBenchmark* --job shortMapperConfigurationconstructor receives user-defined maps viaCreateMap<S,D>()TopologicalOrder()sorts type maps by dependency (child types first)ExpressionBuilder.BuildMappingDelegate()compiles each type map into one of three delegate types:- Fast typed path (
TryBuildTypedDelegate): Single expression tree block with inlined nested maps and flattening — no boxing, no per-property delegates - Flexible path (
BuildFlexibleDelegate): Per-property action arrays for complex features (conditions, hooks, inheritance, MaxDepth) - Context-free path (
TryBuildCtxFreeDelegate):Func<TSource, TDestination>with fully inlined nested objects, collections, and flattening — used by bothMap<S,D>()andMapList<>()
- Fast typed path (
TryBuildCtxFreeListDelegate()compilesFunc<IList<TSource>, List<TDestination>>— entire collection loop + element mapping as a single expression tree- Compiled delegates stored in
FrozenMaps,FrozenCtxFreeMaps, andFrozenCtxFreeListMaps
Mapper.Map<S,D>()checksFastCache<S,D>first (static generic class — zero dict lookup after warm-up)- Falls back to
FrozenCtxFreeMapsfor typedFunc<TSource, TDestination>(zero boxing) - Falls back to
FrozenMapswith thread-staticResolutionContextpooling - Base-type walk + interface walk in
MapInternalfor EF Core proxy / derived type resolution MapList<S,D>()checksFastListCache<S,D>→FrozenCtxFreeListMapsfor fully-inlined collection delegatesIList<T>sources use index-basedforloops (avoids enumerator allocation)
- Inlined nested maps: Child type property assignments are emitted directly into the parent expression tree (no delegate call, no boxing)
- Inlined flattening:
dest.AddressStreet = src.Address.Streetcompiled as direct typed property access - Inlined collection loops: Entire
List<T>mapping loop compiled as single expression tree with inline element mapping - Static generic caching:
FastCache<TSource, TDestination>andFastListCache<TSource, TDestination>eliminate dict lookups
| File | Role |
|---|---|
src/EggMapper/Execution/ExpressionBuilder.cs |
Core compilation — builds all delegate paths, inlining, flattening |
src/EggMapper/Mapper.cs |
Public mapping API, static generic caches, thread-static context |
src/EggMapper/MapperConfiguration.cs |
Orchestrates compilation, stores frozen delegate dictionaries |
src/EggMapper/MapperConfigurationExpression.cs |
Fluent configuration API (CreateMap, profiles, ForMember) |
src/EggMapper/Internal/TypePair.cs |
Value-type dictionary key for source/dest type lookups |
src/EggMapper/Internal/TypeDetails.cs |
Cached reflection metadata (properties, constructors) |
src/EggMapper/Internal/ReflectionHelper.cs |
Utility: numeric/collection type detection, flattening |
src/EggMapper/ServiceCollectionExtensions.cs |
DI registration (AddEggMapper) — transient IMapper, singleton config |
src/EggMapper/ResolutionContext.cs |
Thread-static pooled context with DI ServiceProvider + cycle cache |
| Library | Type | Package |
|---|---|---|
| AutoMapper 16.x | Runtime | AutoMapper |
| Mapster 7.x | Runtime | Mapster |
| Mapperly 4.x | Source generator (compile-time) | Riok.Mapperly |
| AgileMapper 1.x | Runtime | AgileObjects.AgileMapper |
Write code → write tests → dotnet test --configuration Release → fix failures → repeat until 100% green.
Record baseline benchmark → optimize → re-benchmark → compare → repeat until EggMapper beats all competitors on every scenario.
- Zero runtime reflection — no
PropertyInfo.GetValue/SetValuein hot paths - Zero extra allocations — match manual code allocation in every scenario
- No LINQ in hot paths — use
forloops with pre-sized collections AggressiveInliningonMap<S,D>()and delegate lookup methods- Value-type
TypePairfor dictionary keys (no boxing) - Inlined child mappers embedded in parent expression trees (no delegate call overhead)
- EggMapper must be fastest runtime mapper on every benchmark before merging
- Default benchmark target: .NET 10 (
-f net10.0)
- Framework: xUnit + FluentAssertions
- Pattern: AAA (Arrange / Act / Assert)
- Naming:
Feature_Condition_ExpectedBehavior(e.g.,Map_NullSource_ReturnsDefault) - Tests live in
src/EggMapper.UnitTests/
Fully automatic tag-based versioning — every push to main triggers a release:
- Push / merge to
main - CI analyzes commit messages since last tag using conventional commits:
fix:/perf:/chore:→ patch bump (1.1.0 → 1.1.1)feat:→ minor bump (1.1.0 → 1.2.0)BREAKING CHANGE/feat!:→ major bump (1.1.0 → 2.0.0)
- Version derived from last git tag (not csproj) → builds with
-p:Version=→ tests → packs → publishes to NuGet - Creates git tag
v<version>+ GitHub Release with artifacts - Concurrency group serializes publish runs to prevent tag race conditions
No manual version editing needed. No commits pushed back to main. Just use conventional commit prefixes.
- NEVER push directly to
main— always create a feature branch and open a PR - Break complex tasks into smaller incremental commits
- A single PR should focus on one logical change
- Commit and push to the feature branch after each verified, self-contained unit of work
- Use conventional commit prefixes (
feat:,fix:,perf:,chore:,docs:) — the publish pipeline auto-detects version bumps from these