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
<PackageReference Include="Hydrix.Mapper" Version="1.0.0" />
<PackageVersion Include="Hydrix.Mapper" Version="1.0.0" />
<PackageReference Include="Hydrix.Mapper" />
paket add Hydrix.Mapper --version 1.0.0
#r "nuget: Hydrix.Mapper, 1.0.0"
#:package Hydrix.Mapper@1.0.0
#addin nuget:?package=Hydrix.Mapper&version=1.0.0
#tool nuget:?package=Hydrix.Mapper&version=1.0.0
Hydrix.Mapper
⚡ 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,ParentheseswithLower/Uppercasing 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,Bankerrounding - Integer overflow control:
Throw,Clamp,Truncate - Bool to string: six built-in presets (
TrueOrFalse,LowercaseTrueOrFalse,YesOrNo,YOrN,OneOrZero,TOrF) plusCustomwith explicitTrueValue/FalseValuestrings - Enum mapping:
AsString(textual name) orAsInt(underlying integer) viaEnumMapping - Per-property override via
[MapConversion]attribute — read only at cold path, zero runtime cost [NotMapped]support — respectsSystem.ComponentModel.DataAnnotations.Schema- Strict mode — throws on unmatched destination properties
- Nullable source propagation — null guards baked into expression trees
AddHydrixMapperDI extension — registersIHydrixMapperas 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— AutoMapper12.0.1net6.0,net8.0,net10.0— AutoMapper13.0.1
Environment:
- BenchmarkDotNet
0.14.0onnetcoreapp3.1andnet6.0 - BenchmarkDotNet
0.15.8onnet8.0andnet10.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.ps1locally 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:
HydrixMapperclones 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 otherIEnumerable<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)overMap<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
HydrixMapperGlobalConfigurationonly 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 | Versions 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. |
-
.NETCoreApp 3.1
-
net10.0
-
net6.0
-
net8.0
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.