Mappify.NET 1.0.2

dotnet add package Mappify.NET --version 1.0.2
                    
NuGet\Install-Package Mappify.NET -Version 1.0.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Mappify.NET" Version="1.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Mappify.NET" Version="1.0.2" />
                    
Directory.Packages.props
<PackageReference Include="Mappify.NET" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Mappify.NET --version 1.0.2
                    
#r "nuget: Mappify.NET, 1.0.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Mappify.NET@1.0.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Mappify.NET&version=1.0.2
                    
Install as a Cake Addin
#tool nuget:?package=Mappify.NET&version=1.0.2
                    
Install as a Cake Tool

Mappify.NET

A zero-dependency, production-ready object-mapping library for .NET 10 written in C# 14. It replicates the core behaviour of AutoMapper — automatic property mapping, nested-object recursion, collection projection, and a fluent ForMember configuration API — entirely through compiled System.Linq.Expressions delegates that are cached per type-pair for near-zero per-call overhead.

NuGet .NET License: MIT

Installation

dotnet add package Mappify.NET

Table of Contents

  1. Features
  2. Project Structure
  3. Architecture & How It Works
  4. Getting Started
  5. Defining Types
  6. Creating a Mapping Profile
  7. ForMember – All Four Options
  8. Registering Profiles & Building the Mapper
  9. Mapping Methods on IMapper
  10. Nested Object Mapping
  11. Collection Mapping
  12. Type Conversion Rules
  13. Null Safety
  14. Error Handling
  15. Performance Notes
  16. Complete End-to-End Example
  17. API Reference
  18. Limitations

Features

Capability Detail
Automatic property mapping All public readable source properties are mapped to writable destination properties with the same name (case-insensitive).
Nested object recursion Complex property types are recursively mapped, provided a mapping is registered for the nested type-pair.
Collection projection T[], List<T>, IList<T>, ICollection<T>, IEnumerable<T>, IReadOnlyList<T>, IReadOnlyCollection<T>, HashSet<T>, ISet<T>.
Fluent ForMember API Per-property overrides: custom resolver, source-expression, ignored member, constant value.
Compiled expression trees Getter and setter delegates are compiled once per PropertyInfo and stored in a ConcurrentDictionary. Subsequent calls pay zero reflection cost.
Circular-reference guard Configurable maximum depth (default 32). Exceeding it throws a descriptive MappingException.
Null safety Null sources return default(TDestination). Null property values are propagated only to properties that accept null.
Thread-safe MapperConfiguration and Mapper are safe to share across threads and should be treated as singletons.
No third-party dependencies Pure .NET 10 BCL only.

Project Structure

AkzoAutoMapper/
│
├── MappingException.cs        # Typed exception with SourceType / DestinationType
├── TypePair.cs                # Internal readonly struct — cache dictionary key
│
├── IMappingExpression.cs      # Public fluent-API interfaces
├── MemberConfig.cs            # Internal resolver storage (abstract base + generic impl)
├── MappingExpression.cs       # Internal fluent-builder; implements IHasMemberConfigs
│
├── MappingProfile.cs          # Abstract base class — users derive from this
├── MapperConfiguration.cs     # MappingRegistry + MapperConfiguration + IMapperConfigurationExpression
├── IMapper.cs                 # Public IMapper contract
├── Mapper.cs                  # Engine: plan builder, collection mapper, compiled accessors
│
└── Examples/
    └── MappingExamples.cs     # Runnable demonstrations (7 scenarios)

Architecture & How It Works

MapperConfiguration.Create(cfg => cfg.AddProfile<MyProfile>())
        │
        ▼
  MappingRegistry                     ← Dictionary<TypePair, MappingExpression<,>>
        │
        ▼
  MapperConfiguration.CreateMapper()
        │
        ▼
  Mapper  ──── _planCache  ───────────  ConcurrentDictionary<TypePair, Func<obj,obj,Mapper,int,obj>>
                                                      │
                             First call for a type-pair
                                                      │
                                              BuildPlan(srcType, dstType)
                                                      │
                          ┌───────────────────────────┤
                          │                           │
                   ForMember configs            Auto-map by name
                   (custom resolvers)           (case-insensitive)
                          │                           │
                          └──── compiled Action<obj,obj,Mapper,int>[] ──── frozen array
                                                      │
                          Subsequent calls execute the frozen array directly

Plan execution routing (per property, per call)

value is null?
  ├─ dest type is reference / Nullable<T>  → setter(null)
  └─ dest type is non-nullable value type  → skip (leave default)

dest.IsAssignableFrom(src)               → direct setter  [fastest path]

both are collections (non-string IEnumerable)
                                         → MapCollection(...)

both are primitive/scalar
  (bool,byte,char,int,long,float,double,decimal,
   string,DateTime,DateTimeOffset,DateOnly,
   TimeOnly,TimeSpan,Guid, + Nullable<T> of each)
                                         → Convert.ChangeType(value, underlyingType)

both are complex objects (neither is primitive)
                                         → MapCore(value, depth + 1)   [recursive]

otherwise                                → throw MappingException

Compiled getter / setter (built once per PropertyInfo)

// Getter
(object owner) => (object)((OwnerType)owner).Property

// Setter — reference type or non-nullable value type
(object owner, object? value) => ((OwnerType)owner).Property = (PropType)value

// Setter — Nullable<T>
(object owner, object? value) =>
    ((OwnerType)owner).Property = (value == null ? default(T?) : (T?)(T)value)

Getting Started

1 — Reference the project

Add a project reference from your application to AkzoAutoMapper.csproj, or package it as a NuGet library and install the package.

2 — Create a singleton mapper

using AkzoAutoMapper;

// Build configuration once at application start-up.
var config = MapperConfiguration.Create(cfg =>
{
    cfg.AddProfile<UserProfile>();
    cfg.AddProfile<OrderProfile>();
});

// IMapper is thread-safe — share it for the application lifetime.
IMapper mapper = config.CreateMapper();

With ASP.NET Core dependency injection:

// Program.cs
builder.Services.AddSingleton(_ =>
{
    var config = MapperConfiguration.Create(cfg =>
    {
        cfg.AddProfile<UserProfile>();
        cfg.AddProfile<OrderProfile>();
    });
    return config.CreateMapper();
});

Defining Types

Destination types must expose a public parameterless constructor. Properties must be public and either readable (source) or writable (destination).

// Source (domain model)
public class User
{
    public int      Id          { get; set; }
    public string   FirstName   { get; set; } = "";
    public string   LastName    { get; set; } = "";
    public string   Email       { get; set; } = "";
    public DateTime DateOfBirth { get; set; }
}

// Destination (DTO)
public class UserDto
{
    public int      Id          { get; set; }
    public string   FirstName   { get; set; } = "";
    public string   LastName    { get; set; } = "";
    public string   Email       { get; set; } = "";
    public DateTime DateOfBirth { get; set; }
}

Creating a Mapping Profile

Derive from MappingProfile and call CreateMap<TSource, TDestination>() inside the constructor. Every call returns IMappingExpression<TSource, TDestination> for optional ForMember chaining.

public class UserProfile : MappingProfile
{
    public UserProfile()
    {
        // Pure auto-mapping — all property names and types match exactly.
        CreateMap<User, UserDto>();
    }
}

ForMember – All Four Options

ForMember selects a destination property and then routes to one of four behaviours:

Option 1 — MapFrom with a source expression

Best for simple, direct property access. The lambda is compiled once; type conversion between compatible primitives is automatic.

CreateMap<Order, OrderDto>()
    .ForMember(
        d => d.CustomerName,
        o => o.MapFrom(src => src.Customer.FullName));   // expression tree, compiled once

Option 2 — MapFrom with a two-argument resolver (src, dst) => ...

Use for transformations, calculations, string formatting, or when the result depends on the partially-mapped destination object.

CreateMap<Employee, EmployeeDto>()
    // Combine two source fields
    .ForMember(
        d => d.FullName,
        o => o.MapFrom((src, _) => $"{src.FirstName} {src.LastName}".Trim()))

    // Calculation
    .ForMember(
        d => d.MonthlySalary,
        o => o.MapFrom((src, _) => Math.Round(src.AnnualSalary / 12m, 2)))

    // Derived / computed value
    .ForMember(
        d => d.YearsEmployed,
        o => o.MapFrom((src, _) => (int)((DateTime.Today - src.HireDate).TotalDays / 365.25)))

    // Null-coalescing
    .ForMember(
        d => d.Department,
        o => o.MapFrom((src, _) => src.Department ?? "Unassigned"))

    // Using a partially-mapped destination value
    .ForMember(
        d => d.Label,
        o => o.MapFrom((src, dst) => $"{dst.FullName} — {src.Department}"));

Option 3 — Ignore

The destination property is skipped entirely and retains its default value.

CreateMap<User, UserDto>()
    .ForMember(d => d.PasswordHash, o => o.Ignore())
    .ForMember(d => d.InternalNotes, o => o.Ignore());

Option 4 — UseValue

Always assigns a compile-time constant to the destination property.

CreateMap<CsvRow, ProductDto>()
    .ForMember(d => d.Source,    o => o.UseValue("CSV-Import"))
    .ForMember(d => d.IsDeleted, o => o.UseValue(false));

Registering Profiles & Building the Mapper

AddProfile<TProfile>()

Instantiates the profile internally — the simplest option.

var config = MapperConfiguration.Create(cfg =>
{
    cfg.AddProfile<UserProfile>();
    cfg.AddProfile<OrderProfile>();
    cfg.AddProfile<ProductProfile>();
    cfg.AddProfile<EmployeeProfile>();
});

AddProfile(instance)

Use when your profile needs constructor arguments (e.g., injected services).

var config = MapperConfiguration.Create(cfg =>
{
    cfg.AddProfile(new OrderProfile(currencyService));
});

Mapping Methods on IMapper

Method Description
Map<TDest>(object source) Maps using the runtime type of source as the source key. Returns default(TDest) when source is null.
Map<TSrc, TDest>(TSrc source) Maps using TSrc as the compile-time source key. Returns default(TDest) when source is null.
Map<TSrc, TDest>(TSrc source, TDest destination) Populates an existing destination instance. Useful when the destination was created outside the mapper. Returns destination.
MapList<TSrc, TDest>(IEnumerable<TSrc> source) Maps every element and returns a List<TDest>. Returns an empty list when source is null.
// New destination instance (compile-time types)
UserDto dto = mapper.Map<User, UserDto>(user);

// New destination instance (runtime source type)
UserDto dto = mapper.Map<UserDto>(user);

// Populate existing instance
mapper.Map<User, UserDto>(updatedUser, existingDto);

// Collection
List<UserDto> dtos = mapper.MapList<User, UserDto>(users);

Nested Object Mapping

Register a CreateMap for each type-pair that appears anywhere in the object graph. The mapper resolves nested properties recursively with an incremented depth counter.

public class OrderProfile : MappingProfile
{
    public OrderProfile()
    {
        // Must register the nested type-pair separately.
        CreateMap<Address, AddressDto>();

        // Order.ShippingAddress (Address) is mapped to OrderDto.ShippingAddress (AddressDto)
        // automatically, because the Address→AddressDto mapping is registered above.
        CreateMap<Order, OrderDto>()
            .ForMember(
                d => d.PlacedAt,
                o => o.MapFrom((s, _) => s.PlacedAt.ToString("yyyy-MM-dd HH:mm")));
    }
}

Depth limit — the mapper allows up to 32 levels of nesting before throwing MappingException. This prevents runaway recursion on circular object graphs.


Collection Mapping

No extra configuration is required. Property-level collection mapping is automatic when the element types have a compatible mapping registered.

Supported destination collection shapes

Destination type Backing implementation returned
T[] Array.CreateInstance(T, count)
List<T> List<T>
IList<T> List<T>
ICollection<T> List<T>
IEnumerable<T> List<T>
IReadOnlyList<T> List<T>
IReadOnlyCollection<T> List<T>
HashSet<T> HashSet<T>
ISet<T> HashSet<T>
// Using MapList (explicit)
List<ProductDto> dtos = mapper.MapList<Product, ProductDto>(products);

// Via auto-mapped property
public class Catalogue     { public List<Product>    Items { get; set; } = []; }
public class CatalogueDto  { public List<ProductDto> Items { get; set; } = []; }

// No ForMember needed — mapper recurses into Items automatically.
CreateMap<Catalogue, CatalogueDto>();
CreateMap<Product,   ProductDto>();

Type Conversion Rules

When source and destination property types do not match, the mapper applies the following rules in priority order:

Priority Condition Action
1 destType.IsAssignableFrom(srcType) Direct assignment — fastest path
2 Both types are collections MapCollection
3 Both types are primitives / scalars (see table below) Convert.ChangeType
4 Both types are complex objects Recursive MapCore
None of the above MappingException

Primitive / scalar types recognised:

bool, byte, sbyte, char, short, ushort, int, uint, long, ulong, float, double, decimal, string, DateTime, DateTimeOffset, DateOnly, TimeOnly, TimeSpan, Guid, and Nullable<T> wrappers of any of the above.

// int → long  works automatically (Convert.ChangeType)
// int → int?  works automatically (Nullable<T> setter unwraps the type)
// string → Guid  works if the string is a valid GUID

Null Safety

Scenario Behaviour
source argument is null Returns default(TDestination) — never throws
Source property value is null and destination property is a reference type Destination property is set to null
Source property value is null and destination property is Nullable<T> Destination property is set to null (i.e. no value)
Source property value is null and destination property is a non-nullable value type Destination property is left at its default value — no exception
Entire nested object property is null Destination property is set to null (not a new empty instance)
// Safe — returns null, not an exception
UserDto? dto = mapper.Map<User, UserDto>(null!);   // dto == null

// Null nested object
var order = new Order { ShippingAddress = null! };
OrderDto orderDto = mapper.Map<Order, OrderDto>(order);
// orderDto.ShippingAddress == null

Error Handling

All mapping failures throw MappingException — never a raw InvalidCastException, NullReferenceException, or TargetInvocationException.

public sealed class MappingException : Exception
{
    public Type SourceType      { get; }   // the source type at the point of failure
    public Type DestinationType { get; }   // the destination type at the point of failure
    // Message: "Mapping 'FullTypeName' → 'FullTypeName': <reason>"
}

Common causes and fixes

Exception message contains Likely cause Fix
does not expose a public parameterless constructor Destination type has no public T() Add public MyDto() {} or use Map(src, existingDest)
No compatible mapping found for property Incompatible non-primitive property types Add ForMember with a custom resolver
Maximum mapping depth (32) exceeded Circular reference in the object graph Add .ForMember(d => d.BackRef, o => o.Ignore()) on the back-reference
Unsupported destination collection type Collection type not in the supported list Use one of the supported shapes or map manually via ForMember
Cannot convert property Convert.ChangeType failed (e.g. invalid GUID string) Add ForMember with an explicit conversion
try
{
    OrderDto dto = mapper.Map<Order, OrderDto>(order);
}
catch (MappingException ex)
{
    Console.WriteLine(ex.SourceType.Name);       // e.g. "Order"
    Console.WriteLine(ex.DestinationType.Name);  // e.g. "OrderDto"
    Console.WriteLine(ex.Message);               // full explanation
    Console.WriteLine(ex.InnerException);        // original BCL exception, if any
}

Performance Notes

Concern How it is addressed
Reflection per call Zero. Getter and setter delegates are compiled via Expression.Lambda(...).Compile() once per PropertyInfo and stored in static ConcurrentDictionary<PropertyInfo, Func/Action>.
Plan construction per call Zero after first use. The full mapping plan (frozen Action[]) is stored in ConcurrentDictionary<TypePair, Func<...>> keyed on (SourceType, DestinationType).
Memory Plans are never evicted. Memory grows linearly with the number of distinct type-pairs encountered at runtime — typically a few dozen in a real application.
Thread safety ConcurrentDictionary.GetOrAdd ensures at most one plan is compiled per type-pair even under contention.
Nullable setter overhead For Nullable<T> destination properties a one-time conditional expression (value == null ? default(T?) : (T?)(T)value) is compiled into the setter delegate — no branching at the call site.

Complete End-to-End Example

using AkzoAutoMapper;

// ── Models ────────────────────────────────────────────────────────────────────

public class Product
{
    public int     Id         { get; set; }
    public string  Name       { get; set; } = "";
    public decimal Price      { get; set; }
    public int     StockCount { get; set; }
}

public class ProductDto
{
    public int    Id             { get; set; }
    public string Name           { get; set; } = "";
    public string FormattedPrice { get; set; } = "";
    public bool   InStock        { get; set; }
}

// ── Profile ───────────────────────────────────────────────────────────────────

public class ProductProfile : MappingProfile
{
    public ProductProfile()
    {
        CreateMap<Product, ProductDto>()
            .ForMember(d => d.FormattedPrice,
                       o => o.MapFrom((s, _) => $"$ {s.Price:F2}"))
            .ForMember(d => d.InStock,
                       o => o.MapFrom((s, _) => s.StockCount > 0));
    }
}

// ── Application entry point ───────────────────────────────────────────────────

var config = MapperConfiguration.Create(cfg => cfg.AddProfile<ProductProfile>());
IMapper mapper = config.CreateMapper();

// Single object
var product = new Product { Id = 1, Name = "Primer", Price = 24.99m, StockCount = 50 };
ProductDto dto = mapper.Map<Product, ProductDto>(product);
Console.WriteLine($"{dto.Name}  {dto.FormattedPrice}  InStock:{dto.InStock}");
// Primer  $ 24.99  InStock:True

// Collection
var products = new List<Product>
{
    new() { Id = 2, Name = "Topcoat", Price = 89.50m, StockCount = 0 },
    new() { Id = 3, Name = "Varnish", Price = 49.00m, StockCount = 12 }
};
List<ProductDto> dtos = mapper.MapList<Product, ProductDto>(products);
dtos.ForEach(d => Console.WriteLine($"{d.Name}  {d.FormattedPrice}  InStock:{d.InStock}"));
// Topcoat  $ 89.50  InStock:False
// Varnish  $ 49.00  InStock:True

For seven fully worked examples (simple, nested, collection, custom, existing-instance, null-safety, error-handling), run:

AkzoAutoMapper.Examples.MappingExamples.Run();

API Reference

MapperConfiguration

Member Description
static Create(Action<IMapperConfigurationExpression>) Builds the configuration. Call once at startup.
CreateMapper() Returns a thread-safe IMapper backed by this configuration.

IMapperConfigurationExpression

Member Description
AddProfile<TProfile>() Instantiates TProfile (must have a public parameterless constructor) and merges its mappings.
AddProfile(MappingProfile profile) Merges an already-instantiated profile.

MappingProfile

Member Description
CreateMap<TSource, TDestination>() Registers the type-pair and returns IMappingExpression<TSource, TDestination> for fluent chaining.

IMappingExpression<TSource, TDestination>

Member Description
ForMember<TMember>(dest => dest.Prop, opts => ...) Configures a single destination member. Chainable.

IMemberConfigurationExpression<TSource, TDestination, TMember>

Member Description
MapFrom<TSourceMember>(src => src.Prop) Maps via a compiled expression. Supports primitive type conversion.
MapFrom((src, dst) => ...) Maps via a runtime delegate. Full access to source and partial destination.
Ignore() Skips the destination member entirely.
UseValue(TMember value) Sets the destination member to a constant value.

IMapper

Member Description
Map<TDest>(object source) Maps using runtime source type.
Map<TSrc, TDest>(TSrc source) Maps using compile-time source type.
Map<TSrc, TDest>(TSrc source, TDest destination) Maps onto an existing instance.
MapList<TSrc, TDest>(IEnumerable<TSrc> source) Maps every element to List<TDest>.

MappingException

Member Description
SourceType The source Type at the point of failure.
DestinationType The destination Type at the point of failure.
Message "Mapping 'SrcFullName' → 'DstFullName': <reason>"
InnerException The original BCL exception, if any.

Limitations

Limitation Notes
Write-only properties Source properties without a getter and destination properties without a setter are silently skipped.
Constructor injection on destination Destinations must have a public parameterless constructor. Use Map(src, existingDest) to work around this.
Reverse mapping CreateMap<A, B>() does not automatically create a B → A mapping. Call CreateMap<B, A>() explicitly.
Inheritance / polymorphism Map<TSrc, TDest> uses typeof(TSrc) as the lookup key. Map<TDest>(object) uses source.GetType(), which handles runtime polymorphism.
Fields Only properties are mapped. Public fields are ignored.
Plan eviction The plan cache grows for the lifetime of the Mapper instance. In applications with thousands of dynamic types this could become a concern; restart the Mapper if needed.
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.2 120 3/18/2026
1.0.1 108 3/18/2026
1.0.0 113 3/18/2026