Hydrix.Mapper 1.0.0

Prefix Reserved
dotnet add package Hydrix.Mapper --version 1.0.0
                    
NuGet\Install-Package Hydrix.Mapper -Version 1.0.0
                    
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="Hydrix.Mapper" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Hydrix.Mapper" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Hydrix.Mapper" />
                    
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 Hydrix.Mapper --version 1.0.0
                    
#r "nuget: Hydrix.Mapper, 1.0.0"
                    
#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 Hydrix.Mapper@1.0.0
                    
#: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=Hydrix.Mapper&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Hydrix.Mapper&version=1.0.0
                    
Install as a Cake Tool

Hydrix.Mapper

NuGet NuGet Downloads License Quality Gate Status

A high-performance, zero-reflection object mapper for .NET.

Hydrix.Mapper is an object-to-DTO projection library built for developers who demand:

  • Zero reflection on the hot path
  • Predictable, per-property compiled behavior
  • Performance that surpasses AutoMapper across every scenario
  • Full conversion control without custom profiles

Starting with Hydrix.Mapper 1.0.0, every mapping plan is compiled once via expression trees, cached permanently, and executed through a single fused delegate that creates the destination, transfers every property, and returns — with no per-call reflection, no hidden allocations, and no delegate overhead beyond the plan call itself.


🧭 Why Hydrix.Mapper?

Hydrix.Mapper is designed for systems where:

  • DTO projection happens on every request and latency matters
  • Teams need explicit, auditable per-property conversion rules
  • Configuration must be done once, at startup, with zero runtime cost
  • Thread safety is required without locks on the hot path

Hydrix.Mapper does not attempt to infer your mapping intent from conventions alone. You configure it, it compiles it, and it runs it fast.


⚠️ What Hydrix.Mapper is not

  • A query language or LINQ provider
  • A deep-graph serializer or recursive mapper
  • A runtime-configurable mapper (plans are compiled at first use and are immutable)
  • A replacement for AutoMapper in projects that rely on AutoMapper's profile system

⚙️ Supported frameworks

  • .NET Core 3.1
  • .NET 6
  • .NET 8
  • .NET 10

✨ Key Features

  • Single fused compiled delegate per type pair — destination construction and all property transfers in one expression block
  • Per-instance fast cache keyed by (Type source, Type dest) — eliminates option-key construction on every hot-path call
  • Typed local variables in compiled expressions — each cast is emitted once regardless of property count
  • Identity reference-type assignments skip the null-check wrapper — direct assignment handles null naturally
  • String transforms: Trim, TrimStart, TrimEnd, Uppercase, Lowercase, and combinations
  • Guid formatting: Hyphenated, DigitsOnly, Braces, Parentheses with Lower/Upper casing control
  • DateTime and DateTimeOffset to string: custom format, timezone normalization (None, ToUtc, ToLocal), and culture
  • DateOnly to string (.NET 6+)
  • Decimal and float to integral: Truncate, Ceiling, Floor, Nearest, Banker rounding
  • Integer overflow control: Throw, Clamp, Truncate
  • Bool to string: six built-in presets (TrueOrFalse, LowercaseTrueOrFalse, YesOrNo, YOrN, OneOrZero, TOrF) plus Custom with explicit TrueValue/FalseValue strings
  • Enum mapping: AsString (textual name) or AsInt (underlying integer) via EnumMapping
  • Per-property override via [MapConversion] attribute — read only at cold path, zero runtime cost
  • [NotMapped] support — respects System.ComponentModel.DataAnnotations.Schema
  • Strict mode — throws on unmatched destination properties
  • Nullable source propagation — null guards baked into expression trees
  • AddHydrixMapper DI extension — registers IHydrixMapper as singleton
  • Convenience extension methods: ToDto<TDest>(), ToDtoList<TDest>()
  • No non-Microsoft runtime dependencies
  • Apache 2.0 licensed

📊 Benchmark Snapshot vs AutoMapper

The benchmark suite compares Hydrix.Mapper against AutoMapper across flat-object widths, collection sizes, type conversion scenarios, and cold-path plan compilation.

Baseline versions by target framework:

  • netcoreapp3.1 — AutoMapper 12.0.1
  • net6.0, net8.0, net10.0 — AutoMapper 13.0.1

Environment:

  • BenchmarkDotNet 0.14.0 on netcoreapp3.1 and net6.0
  • BenchmarkDotNet 0.15.8 on net8.0 and net10.0
  • Host runtime for the published snapshot: .NET 10.0.5 · X64 RyuJIT AVX2
  • Job: LongRun (100 iterations, 3 launches, 15 warmups)

Single object — flat

Scenario Hydrix.Mapper AutoMapper Gain
flat small (5 props) 18 ns 37 ns ~51% faster
flat medium (12 props) 26 ns 47 ns ~44% faster
flat large (20 props) 28 ns 48 ns ~42% faster

Single object — with conversions

Scenario Hydrix.Mapper AutoMapper Gain
string trim + guid + datetime + decimal→int 66 ns 89 ns ~27% faster

Collections — speed

Scenario Hydrix.Mapper AutoMapper Gain
list small ×100 893 ns 1,324 ns ~33% faster
list medium ×100 1,665 ns 2,143 ns ~22% faster
list large ×100 1,795 ns 2,304 ns ~22% faster
list small ×1000 9,417 ns 11,293 ns ~17% faster
list medium ×1000 16,860 ns 18,607 ns ~9% faster
list large ×1000 18,220 ns 20,610 ns ~12% faster

Collections — allocations

Scenario Hydrix.Mapper AutoMapper Reduction
list small ×100 5,696 B 6,992 B ~19% less
list medium ×100 12,096 B 13,392 B ~10% less
list large ×100 16,096 B 17,392 B ~7% less
list small ×1000 56,096 B 64,600 B ~13% less
list medium ×1000 120,096 B 128,600 B ~7% less
list large ×1000 160,096 B 168,600 B ~5% less

Cold path

Scenario Hydrix.Mapper
first hit (plan compile + execute) ~453 ns

The cold path cost is paid exactly once per type pair per application lifetime. Every subsequent call uses the cached compiled plan with no reflection.

Benchmark results are updated with each release. Run benchmark.ps1 locally for your hardware profile.


📦 Installation

dotnet add package Hydrix.Mapper

🚀 Basic Usage

Map a single object

var mapper = new HydrixMapper(new HydrixMapperOptions());

var dto = mapper.Map<UserDto>(user);

Map with compile-time source type

var dto = mapper.Map<User, UserDto>(user);

Map a list

// Typed — single plan resolved once before the loop
IReadOnlyList<UserDto> dtos = mapper.MapList<User, UserDto>(users);

// Untyped — resolves plan per unique source runtime type
IReadOnlyList<UserDto> dtos = mapper.MapList<UserDto>(sources);

Extension methods

using Hydrix.Mapper.Extensions;

var dto = user.ToDto<UserDto>();

IReadOnlyList<UserDto> dtos = users.ToDtoList<User, UserDto>();

🧩 Configuration & DI

Standalone

var options = new HydrixMapperOptions();
options.String.Transform = StringTransforms.Trim;
options.Guid.Format     = GuidFormat.Hyphenated;
options.Guid.Case       = GuidCase.Lower;

var mapper = new HydrixMapper(options);

Dependency injection

using Hydrix.Mapper.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;

services.AddHydrixMapper(options =>
{
    options.String.Transform              = StringTransforms.Trim;
    options.Guid.Format                   = GuidFormat.Hyphenated;
    options.Guid.Case                     = GuidCase.Lower;
    options.DateTime.StringFormat         = "O";
    options.DateTime.TimeZone             = DateTimeZone.None;
    options.Numeric.DecimalToIntRounding  = NumericRounding.Truncate;
    options.Numeric.Overflow              = NumericOverflow.Clamp;
    options.Bool.StringFormat             = BoolStringFormat.LowercaseTrueOrFalse;
});

IHydrixMapper is registered as a singleton. Inject it wherever you need projection:

public class UserService(IHydrixMapper mapper)
{
    public UserDto GetUser(User user) => mapper.Map<User, UserDto>(user);
}

AddHydrixMapper only registers an isolated IHydrixMapper singleton inside the target IServiceCollection. It does not mutate the process-wide mapper used by ToDto() and ToDtoList().

If you want to configure the global extension-method mapper explicitly, use HydrixMapperGlobalConfiguration:

using Hydrix.Mapper.Configuration;

HydrixMapperGlobalConfiguration.Configure(options =>
{
    options.String.Transform = StringTransforms.Uppercase;
});

Configuration lifecycle and thread safety

HydrixMapperOptions is a mutable configuration builder. Configure it before mapper creation and do not mutate the same instance concurrently while it is being used to create mappers.

Lifecycle rules:

  • HydrixMapper clones the supplied options in its constructor. Later mutations to the original options instance do not affect that mapper.
  • AddHydrixMapper(...) captures an isolated options snapshot for the service collection where it is registered.
  • HydrixMapperGlobalConfiguration.Configure(...) captures a new global snapshot used only by the convenience extension methods.
  • Reconfigure once at startup whenever possible. Avoid mutating shared options objects at runtime.

Nested Mapping Rules

Nested mapping is explicit and exact by design.

Hydrix.Mapper only performs nested mapping when the source property type is an exact match for the registered nested source type:

sourcePropType == registeredSourceType

Nested mapping is resolved from:

  • options.MapNested<TSource, TDest>()
  • [MapFrom(typeof(TSource))] on the destination type

Supported exact-match example

var options = new HydrixMapperOptions();
options.MapNested<AddressEntity, AddressDto>();

public sealed class CustomerEntity
{
    public AddressEntity Address { get; set; }
}

public sealed class CustomerDto
{
    public AddressDto Address { get; set; }
}

This works because the nested source property type is exactly AddressEntity, which is the registered source type for AddressDto.

Not supported: inheritance

var options = new HydrixMapperOptions();
options.MapNested<AddressEntity, AddressDto>();

public sealed class PremiumAddressEntity : AddressEntity
{
}

public sealed class CustomerEntity
{
    public PremiumAddressEntity Address { get; set; }
}

public sealed class CustomerDto
{
    public AddressDto Address { get; set; }
}

This does not trigger nested mapping because PremiumAddressEntity != AddressEntity.

Not supported: interfaces

public interface IAddress
{
    string Street { get; }
}

var options = new HydrixMapperOptions();
options.MapNested<IAddress, AddressDto>();

public sealed class CustomerEntity
{
    public AddressEntity Address { get; set; }
}

This does not trigger nested mapping because the actual nested source property type is AddressEntity, not IAddress.

Nested collection element rule

The same exact-match rule applies to nested collections. A destination collection of OrderDto only maps automatically when the source element type is exactly the registered source type for OrderDto.


Nested Collection Support

The table below describes destination property support for nested collection mapping.

Destination property type Supported Notes
List<T> Yes Concrete fast path
IList<T> Yes Result is built as List<T> and assigned through the interface
IReadOnlyList<T> Yes Result is built as List<T> and assigned through the interface
IEnumerable<T> Yes Result is built as List<T> and assigned through the interface
ICollection<T> No Rejected with an explicit exception
IReadOnlyCollection<T> No Rejected with an explicit exception
Arrays (T[]) No Rejected with an explicit exception
Custom collection types No Rejected with an explicit exception

Notes:

  • This contract applies to nested destination properties.
  • Source collections may be List<T>, IList<T>, arrays, or other IEnumerable<T> implementations.
  • Unsupported nested destination collection types fail fast with a descriptive error instead of falling back to undefined behavior.

🔄 Conversion Options

String transforms

options.String.Transform = StringTransforms.Trim;        // "  Alice  " → "Alice"
options.String.Transform = StringTransforms.Uppercase;  // "alice" → "ALICE"
options.String.Transform = StringTransforms.Trim | StringTransforms.Lowercase; // "  Alice  " → "alice"

Guid format

options.Guid.Format = GuidFormat.Hyphenated;    // 00000000-0000-0000-0000-000000000000
options.Guid.Format = GuidFormat.DigitsOnly;    // 00000000000000000000000000000000
options.Guid.Format = GuidFormat.Braces;        // {00000000-0000-0000-0000-000000000000}
options.Guid.Format = GuidFormat.Parentheses;   // (00000000-0000-0000-0000-000000000000)
options.Guid.Case   = GuidCase.Upper;           // uppercase letters

DateTime to string

options.DateTime.StringFormat = "O";                    // ISO 8601 round-trip
options.DateTime.TimeZone     = DateTimeZone.ToUtc;     // normalize to UTC before formatting
options.DateTime.Culture      = "pt-BR";                // culture-aware formatting

Numeric rounding and overflow

options.Numeric.DecimalToIntRounding = NumericRounding.Nearest;   // Math.Round MidpointRounding.AwayFromZero
options.Numeric.Overflow             = NumericOverflow.Clamp;     // clamp to target type bounds

Bool to string

options.Bool.StringFormat = BoolStringFormat.YesOrNo;              // "Yes" / "No"
options.Bool.StringFormat = BoolStringFormat.LowercaseTrueOrFalse; // "true" / "false"
options.Bool.StringFormat = BoolStringFormat.OneOrZero;            // "1" / "0"
options.Bool.StringFormat = BoolStringFormat.Custom;
options.Bool.TrueValue    = "Ativo";
options.Bool.FalseValue   = "Inativo";

🏷️ Per-Property Overrides

Use [MapConversion] on any destination property to override global options for that property only. The attribute is read at plan compilation — zero runtime cost.

public class UserDto
{
    public string Name { get; set; }

    [MapConversion(GuidFormat = GuidFormat.DigitsOnly, GuidCase = GuidCase.Upper)]
    public string ExternalId { get; set; }

    [MapConversion(DateFormat = "dd/MM/yyyy", DateTimeZone = DateTimeZone.ToLocal)]
    public string CreatedAt { get; set; }

    [MapConversion(NumericRounding = NumericRounding.Nearest)]
    public int Score { get; set; }

    [MapConversion(BoolFormat = BoolStringFormat.YesOrNo)]
    public string IsActive { get; set; }
}

🔒 Strict Mode

Enable strict mode to throw at plan-compile time when a destination property has no matching source property:

options.StrictMode = true;

Useful during development to catch renaming mismatches early. Disable in production for forward-compatible DTOs.


Best Practices for Maximum Performance

  • Prefer Map<TSource, TTarget>(source) over Map<TTarget>(object) in hot paths.
  • Prefer MapList<TSource, TTarget>(sources) over untyped list mapping in tight loops.
  • Reuse mapper instances instead of constructing them per request.
  • Configure once at startup, then treat mapper configuration as immutable.
  • Use DI for application-scoped mapper instances and HydrixMapperGlobalConfiguration only when you intentionally want a process-wide default for the extension methods.

🎯 Design Philosophy

Hydrix.Mapper is built around the following principles:

  • Compile once, execute indefinitely
  • Zero reflection on the hot path
  • Performance first, without sacrificing correctness
  • Explicit conversion rules baked into compiled expressions
  • Thread-safe by design — no locks on the hot path

❤️ Supporting Hydrix

Hydrix is an open-source project built and maintained with care, transparency, and a long-term vision.

If Hydrix.Mapper helps you build reliable, predictable, and high-performance projection layers, consider supporting the project. Your support helps ensure ongoing maintenance, improvements, documentation, and long-term sustainability.

You can support Hydrix through GitHub Sponsors:

👉 https://github.com/sponsors/marcelo-mattos

Every contribution, whether financial or by sharing feedback and usage experiences, is deeply appreciated.


📄 License

This project is licensed under the Apache License 2.0. See the LICENSE and NOTICE files for details.


👨‍💻 Author

Marcelo Matos dos Santos Software Engineer • Open Source Maintainer. Engineering clarity. Delivering transformation.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  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. 
.NET Core netcoreapp3.1 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.0 39 4/9/2026

Hydrix.Mapper 1.0.0 — initial public release.

     Hydrix.Mapper outperforms the benchmarked AutoMapper baseline across
     every measured scenario (AutoMapper 12.0.1 on netcoreapp3.1;
     AutoMapper 13.0.1 on net6.0, net8.0, and net10.0; LongRun, 100
     iterations, 3 launches, .NET 10.0.5, X64 RyuJIT AVX2):
     flat small ~51% faster, flat medium ~44% faster, flat large ~42% faster,
     conversions ~27% faster, and lists 9–33% faster with 5–19% fewer
     allocations. Cold path: ~453 ns per type pair.

     Architecture highlights:

     - Single fused compiled delegate per type pair. Destination construction
       and all property transfers happen in one expression block, eliminating
       separate factory and mapper delegate invocations from the hot path.

     - Typed compiled delegate (Func[TSource, TTarget]) per plan. The generic
       typed API invokes it directly, eliminating boundary casts from the hot
       path loop.

     - Identity reference-type optimization. When no conversion is applied to a
       reference-type property, the null-check Condition expression is omitted
       from the compiled IL entirely — direct assignment handles null naturally.

     - Typed local variables in compiled expressions. Each source and destination
       cast is emitted once regardless of property count, reducing redundant
       castclass instructions.

     - Per-instance fast cache keyed only by (Type source, Type dest) pair.
       Eliminates option-key construction and hash computation on every hot-path
       call.

     - Global MapPlanCache keyed by source type, destination type, and option
       snapshot. Plans are compiled at most once per unique combination across
       all mapper instances.

     - CollectionsMarshal.AsSpan fast path for typed list mapping on .NET 6+.
       IList[T] index-loop fast path on all targets. Pre-sized result buffers.

     - Comprehensive conversion suite: string transforms (Trim, TrimStart,
       TrimEnd, Uppercase, Lowercase, combinations), Guid formatting
       (Hyphenated, DigitsOnly, Braces, Parentheses with Lower/Upper casing),
       DateTime/DateTimeOffset/DateOnly to string (format, timezone
       normalization, culture), bool to string (6 presets: TrueOrFalse,
       LowercaseTrueOrFalse, YesOrNo, YOrN, OneOrZero, TOrF — plus Custom),
       decimal/float to integral (5 rounding strategies), integer overflow
       control (Throw/Clamp/Truncate), and enum mapping (AsString/AsInt).

     - Nested object and collection mapping via MapNested[TSrc, TDest]()
       with compile-time circular reference detection.

     - Per-property [MapConversion] attribute override read only at plan-compile
       time — zero runtime cost.

     - [NotMapped] support, strict mode, nullable source propagation, AddHydrixMapper
       DI extension, ToDto and ToDtoList convenience extension methods.

     - Multi-targeted: net10.0, net8.0, net6.0, netcoreapp3.1.

     - 247/243 unit tests with 100% line/branch/method coverage on all targets.