MeliorMapper 1.0.0

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

MeliorMapper

MeliorMapper is a high-performance, expression-tree based object-object mapper for .NET. It targets .NET 8, .NET 9, and .NET 10 with an identical public API on every framework.

Status: 1.0.0 - Phase 4 (LINQ, tooling, and ecosystem) complete. See ROADMAP.md for the full per-phase breakdown, and CHANGELOG.md for release history.

Why MeliorMapper

  • No reflection at map time. All mapping logic is compiled once, per type pair, into cached expression-tree delegates. Reflection is used only while building that configuration.
  • Thread-safe by design. The compiled delegate cache is a ConcurrentDictionary, and every Map call gets its own resolution context - no locks on the hot path.
  • Convention-based, zero-config for simple DTOs. Nested types, collections, and dictionaries encountered during mapping that weren't explicitly registered are mapped automatically by matching member names - regardless of naming convention - so you don't have to declare every nested CreateMap.
  • Circular-reference safe. Reference-typed object graphs with cycles map without stack overflows and preserve reference identity across the cycle.
  • AutoMapper-shaped configuration surface. ForMember, ForPath, ReverseMap, Profiles, value resolvers/converters - the fluent API will feel immediately familiar.

Install

dotnet add package MeliorMapper
dotnet add package MeliorMapper.DependencyInjection  # optional, for services.AddMeliorMapper(...)

Quick start

using MeliorMapper;

var config = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<User, UserDto>();
});

var mapper = config.CreateMapper();

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

Features (Phase 1 - core engine)

  • MapperConfiguration / IMapper / CreateMap<TSource, TDestination>()
  • Convention-based property and field matching
  • Constructor mapping for destination types without a public parameterless constructor (including default parameter values for unmatched parameters)
  • Nested object mapping
  • Collection mapping: List<T>, arrays, HashSet<T> / ISet<T>, and other IEnumerable<T>-shaped destinations
  • Dictionary mapping (IDictionary<TKey,TValue> and Dictionary<TKey,TValue>)
  • Primitive, string, and numeric conversions (including a Convert.ChangeType fallback for otherwise-unconvertible pairs)
  • Nullable value type mapping
  • Enum-to-enum, enum-to-string, and string-to-enum mapping
  • Circular reference protection with reference-identity preservation
  • Fully compiled, cached mapping delegates (ConcurrentDictionary-backed, lock-free reads)
// Map onto an existing instance
mapper.Map(source, existingDestination);

// Map using the runtime type of a boxed source
UserDto dto = mapper.Map<UserDto>((object)user);

// Types not explicitly registered are still mapped by convention
var addressDto = mapper.Map<AddressDto>(address);

Features (Phase 2 - advanced configuration)

Profiles, assembly scanning, and DI

public class UserProfile : Profile
{
    public UserProfile()
    {
        CreateMap<User, UserDto>();
    }
}

var config = new MapperConfiguration(cfg =>
{
    cfg.AddProfile<UserProfile>();
    // or: cfg.AddMapsFromAssembly(typeof(UserProfile).Assembly);
});
services.AddMeliorMapper(cfg => cfg.AddProfile<UserProfile>());
// registers MapperConfiguration and IMapper as singletons

ForMember / ForPath

cfg.CreateMap<Order, OrderDto>()
    .ForMember(d => d.Total, opt => opt.MapFrom((s, _) => s.UnitPrice * s.Quantity))
    .ForMember(d => d.Notes, opt => opt.NullSubstitute("(none)"))
    .ForMember(d => d.Discount, opt => opt.Condition(s => s.IsMember))
    .ForPath(d => d.Shipping.City, opt => opt.MapFrom(s => s.ShippingCity));

Per-member options: MapFrom (expression or Func), Ignore, Condition, PreCondition, NullSubstitute, ConvertUsing (a Func or an IValueConverter<TSourceMember, TMember>), ResolveUsing (a Func or an IValueResolver<TSource, TDestination, TMember>).

MapFrom expressions that are a simple chain of member accesses (s => s.Address.City) are decomposed and emitted as null-safe inline code, exactly like automatic flattening below - no delegate call, no NullReferenceException if Address is null. Anything else (method calls, arithmetic, string interpolation) is compiled once, at configuration time, into a plain delegate.

ReverseMap, Include, IncludeBase, IncludeAllDerived

cfg.CreateMap<Address, AddressDto>().ReverseMap();

cfg.CreateMap<Animal, AnimalDto>()
    .ForMember(d => d.Species, opt => opt.MapFrom(s => s.GetType().Name))
    .IncludeAllDerived();
cfg.CreateMap<Dog, DogDto>();   // inherits the Species configuration above

Include/IncludeBase/IncludeAllDerived merge member configuration (ForMember, BeforeMap, AfterMap) between a base and derived type-pair at configuration time. This is not yet runtime polymorphic dispatch (mapping the runtime-derived type from a base-typed reference) - that is planned for a later phase.

ConstructUsing / ConvertUsing / BeforeMap / AfterMap

cfg.CreateMap<OrderRow, Order>().ConstructUsing(row => new Order(row.Id, row.Sku));

cfg.CreateMap<Money, decimal>().ConvertUsing((m, _) => m.Amount);
cfg.CreateMap<Address, AddressDto>().ConvertUsing<UpperCaseAddressConverter>();

cfg.CreateMap<User, UserDto>()
    .BeforeMap((src, dest) => dest.MappedAtUtc = DateTime.UtcNow)
    .AfterMap((src, dest) => dest.DisplayName = dest.DisplayName.Trim());

Naming conventions and flattening

Member matching normalizes case and separators, so PascalCase, camelCase, snake_case, kebab-case, and UPPER_CASE all match each other automatically - no configuration needed.

public class Customer { public Address Address { get; set; } }
public class Address { public string City { get; set; } }

public class CustomerDto { public string AddressCity { get; set; } }

cfg.CreateMap<Customer, CustomerDto>(); // AddressCity <- Customer.Address.City, automatically

Configuration validation

var config = new MapperConfiguration(cfg => cfg.CreateMap<User, UserDto>());
config.AssertConfigurationIsValid(); // throws MeliorMapperConfigurationException listing every
                                      // destination member that isn't mapped, ignored, or
                                      // constructor-populated

Features (Phase 3 - enterprise features)

Records, init-only properties, required members, private/internal setters

All of these already work with no special configuration, because the compiled expression trees MeliorMapper generates bypass the C# compiler's restrictions on init/required (the CLR itself never enforces them) and can invoke non-public property setters directly:

public record PersonRecord(int Id, string Name);           // constructor-matched, as before

public class PersonDto
{
    public required int Id { get; init; }                  // init-only + required
    public string Name { get; internal set; } = "";         // internal setter
}

cfg.CreateMap<PersonSource, PersonDto>();                   // just works

Generic and open-generic mapping

cfg.CreateMap<Container<int>, ContainerDto<int>>();         // ordinary closed generics - always worked

cfg.CreateMap(typeof(Repository<>), typeof(RepositoryDto<>)); // open-generic template; every
                                                               // closed instantiation is mapped
                                                               // by convention, compiled lazily
cfg.CreateMap(sourceType, destinationType);                 // also handy when types are only
                                                              // known via reflection (e.g. a loop)

Open-generic registrations carry no ForMember customization (there's no way to write a typed member expression without a concrete type argument) and are skipped by AssertConfigurationIsValid.

Interface and abstract class mapping

Reading from an interface-typed source now sees members declared on every interface it extends, not just the interface itself. Mapping into an interface or abstract class requires ConstructUsing (there's no constructor to select otherwise) - MeliorMapper throws a clear MeliorMapperConfigurationException if you don't provide one:

cfg.CreateMap<PersonSource, IPerson>().ConstructUsing(_ => new PersonImpl());

Dynamic mapping: ExpandoObject, Dictionary<string, object>, Dictionary<object, object>

ExpandoObject bag = mapper.Map<Address, ExpandoObject>(address);   // one entry per member;
                                                                    // nested complex members
                                                                    // become nested dictionaries
Address back = mapper.Map<ExpandoObject, Address>(bag);            // entries matched by
                                                                    // normalized key name

Dictionary<object, object> is handled by the existing dictionary-to-dictionary path. Converting a dictionary into a POCO necessarily inspects each value's runtime type (value.GetType()) - the one deliberate, narrowly-scoped exception to "no reflection while mapping"; every statically-typed mapping remains fully reflection-free.

Global filters: MaxDepth, PreserveReferences, IgnoreNullValues, AllowNullCollections, AllowNullDestinationValues

cfg.CreateMap<Node, NodeDto>().MaxDepth(3);                 // self-referencing trees: stop
                                                             // recursing past 3 levels (nulls out)
cfg.CreateMap<User, UserDto>()
    .IgnoreNullValues()                 // null source value -> leave destination member as-is
    .AllowNullCollections(false)        // null source collection -> empty destination collection
    .AllowNullDestinationValues(false); // null complex member -> new() instead of null

PreserveReferences() is accepted for API familiarity; MeliorMapper always preserves reference identity across circular class graphs, so there's nothing extra to opt into.

Global value transformers

cfg.AddGlobalValueTransformer<string>(s => s?.Trim()!);     // applied to every string member,
                                                             // across every map, after any
                                                             // ForMember override has resolved it

Runtime diagnostics

cfg.EnableDiagnostics(entry => logger.LogDebug(
    "{Source} -> {Destination} in {Duration}", entry.SourceType, entry.DestinationType, entry.Duration));

config.Diagnostics.TotalMaps;
config.Diagnostics.CountFor<User, UserDto>();

Disabled by default - a single null check per top-level Map call, no Stopwatch, no counters, until EnableDiagnostics is called.

Features (Phase 4 - LINQ, tooling, and ecosystem)

ProjectTo / IQueryable

var dtos = dbContext.Blogs.ProjectTo<Blog, BlogDto>(mapper).ToList();

Builds a pure LINQ expression tree - member access, object/collection initialization, simple conversions only, with no call back into MeliorMapper anywhere in the tree - so EF Core (or any other IQueryable provider) translates the whole projection instead of materializing entities into memory first. Verified against a real SQLite-backed EF Core query, not just LINQ-to-Objects. Supports direct/flattened/MapFrom(Expression) member matching and nested objects/collections; throws a clear MeliorMapperConfigurationException for anything that can't be expressed as a pure expression tree (MapFrom(Func), ResolveUsing, member ConvertUsing, Condition/PreCondition, ConstructUsing, type-level ConvertUsing, BeforeMap/AfterMap, ForPath) - point that particular map at .ToList() first and use Map() afterward instead.

Mapping report

File.WriteAllText("mapping-report.html", config.GenerateHtmlReport());

Static HTML report of every registered type map: which destination member binds to which source member (direct/flattened/ForMember), which are unmapped or ignored, and which type-map-level features (ConstructUsing, ConvertUsing, MaxDepth, before/after hooks) are in play. Purely a diagnostic/documentation tool - it has no effect on mapping behavior.

Roslyn analyzer

MeliorMapper.Analyzers (add as a <PackageReference> with PrivateAssets="all") is a best-effort syntax/symbol check - it doesn't replicate the runtime engine's flattening/naming-convention normalization or execute MeliorMapper itself, so it may occasionally warn on something the engine would actually resolve. Diagnostics:

Id Severity What it flags
MELIORMAP001 Warning A destination member with no matching source member and no ForMember/ForPath in the chain (has a code fix: insert .ForMember(d => d.X, opt => opt.Ignore())).
MELIORMAP002 Warning CreateMap<T, T>() - source and destination are the same type, so the registration is a no-op.
MELIORMAP003 Warning The same source/destination type pair registered via CreateMap more than once in the compilation.
MELIORMAP004 Info Both CreateMap<A, B>() and CreateMap<B, A>() registered separately - suggests .ReverseMap() instead.
MELIORMAP005 Warning A Profile subclass defined but never passed to AddProfile (suppressed entirely if any AddMapsFromAssembly call exists anywhere, since that sweeps profiles up reflectively in a way this check can't verify).

Deliberately not implemented: an "unused mapping" diagnostic (finding every real usage of a registered pair across an arbitrary consuming codebase has a high false-positive rate that would undermine trust in the whole analyzer) and "invalid property name" checks (MeliorMapper's API is fully strongly-typed - ForMember(d => d.X, ...) - so there's no string-based property name to be invalid in the first place).

Project layout

src/MeliorMapper.Abstractions       Public contracts + Profile/CreateMap recording (no compilation)
src/MeliorMapper                    Core mapping engine (expression-tree compiler, delegate cache,
                                   ProjectTo/report generation)
src/MeliorMapper.DependencyInjection  services.AddMeliorMapper(...)
src/MeliorMapper.Analyzers          Roslyn analyzer (MELIORMAP001-005) + code fix
src/MeliorMapper.Generator          Incremental Roslyn source generator (Native AOT/trim-friendly,
                                   scoped subset - see "Source generator" above)
tests/MeliorMapper.Tests            xUnit + FluentAssertions test suite (100+ tests)
tests/MeliorMapper.Analyzers.Tests  Analyzer tests (direct Roslyn CompilationWithAnalyzers harness)
tests/MeliorMapper.Generator.Tests  Generator tests (runs the real generator, emits and loads the
                                   resulting assembly, invokes the generated methods)
tests/MeliorMapper.Benchmarks       BenchmarkDotNet: MeliorMapper vs manual mapping vs AutoMapper vs Mapster
samples/ConsoleSample             Minimal end-to-end usage sample (Profile, ForMember, ReverseMap)
samples/MinimalApiSample          ASP.NET Core minimal API (DI integration, ProjectTo over an
                                   in-memory IQueryable)
samples/WorkerServiceSample       Background queue-processing worker (Profile, ForMember, DI)
samples/GeneratorSample           [GenerateMapper] usage; the Native AOT publish test above ran
                                   against this project
samples/BlazorSample              Blazor Server app: DI-injected IMapper used from a Razor
                                   component to map an internal entity to a view-facing DTO
docs/                             Architecture, Migration, Performance, and Best Practices guides

Native AOT (honest finding)

MeliorMapper's runtime engine is not currently compatible with strict Native AOT publishing (dotnet publish -p:PublishAot=true). This was empirically tested (samples/ConsoleSample, win-x64, self-contained) rather than assumed, and it fails - not in one isolated spot, but throughout the compiler: dozens of IL2026/IL2070/IL3050 trim/AOT-analyzer errors from System.Reflection calls (GetProperties, GetConstructors, MakeGenericMethod, MakeGenericType, Activator.CreateInstance) and from .NET's own System.Linq.Expressions APIs (Expression.Lambda(...).Compile(), Expression.Property(...)), which the BCL itself marks RequiresDynamicCode/RequiresUnreferencedCode.

This is an earlier over-claim being corrected: the package description and this README previously said "Native AOT friendly," which isn't accurate for the runtime engine as built. It's fixed now. AddMapsFromAssembly/AddMapsFromAssemblies specifically are additionally annotated with [RequiresUnreferencedCode] (they reflect over an entire assembly to find Profile types, which trimming can't reason about) so calling them at least surfaces a warning in a trimmed/AOT build.

Why this is architectural, not a quick fix: MeliorMapper's whole design is "reflect once at configuration time, then compile and cache an expression tree" - and expression-tree compilation itself requires the code generation Native AOT specifically forbids. Annotating every reflection call would only silence the warnings, not change the underlying runtime risk (a MakeGenericMethod call for a type combination the AOT compiler didn't see ahead of time can fail at runtime, not just warn at publish time). Genuine Native AOT support needs a fundamentally different code path - see the source generator below, which now exists and was tested against exactly this scenario.

Source generator (MeliorMapper.Generator)

A separate, narrower way to get mapping code, specifically built to answer the Native AOT gap above: an incremental Roslyn source generator that emits plain, ordinary C# at compile time - no reflection, no System.Linq.Expressions, nothing for a trim/AOT analyzer to object to. It is not a replacement for the runtime CreateMap-based IMapper and uses a different, attribute- driven API (a compile-time generator can't see a runtime configuration lambda, so CreateMap's fluent API doesn't apply here):

[MeliorMapper.GenerateMapper]
public partial class OrderMapper
{
    public partial OrderDto Map(Order source);
}

var mapper = new OrderMapper();
var dto = mapper.Map(order); // body generated at compile time

Declare a partial class with [GenerateMapper] and one partial method per mapping shaped like partial TDestination MethodName(TSource source); the generator fills in the body.

What's supported: exact/case-insensitive property-name matching, class destinations with a public parameterless constructor and public settable properties, nested object members (mapped recursively, inline in the same generated method), List<T>/array/IEnumerable<T>-shaped collection members, numeric widening/narrowing, and enum↔string conversion.

What's deliberately out of scope (each reports a compile-time diagnostic - MELIORMAPGEN001 through MELIORMAPGEN006 - rather than silently generating something wrong or incomplete): ForMember-equivalent customization, ReverseMap, Profiles, records/constructor-only mapping (no public parameterless constructor), dictionaries, and self-referencing type graphs (the generator has no runtime circular-reference guard, so a type that maps to itself again would recurse infinitely at compile time instead - it reports MELIORMAPGEN001 and leaves that method ungenerated instead). Use the runtime CreateMap-based IMapper for anything in this list.

Verified, not just claimed: samples/GeneratorSample runs a generated mapper (nested objects, a collection, an enum→string conversion) and produces correct output. More importantly, dotnet publish -c Release -r win-x64 --self-contained -p:PublishAot=true against that sample compiled and passed IL trim/AOT analysis with zero IL20xx/IL30xx errors or warnings - a stark contrast to the runtime engine's dozens of failures documented above. (The publish only got as far as the native-linking step on this particular machine, which lacks the MSVC C++ linker toolchain Native AOT needs to produce the final executable - an environment gap, not a code one; the part that actually proves code-level AOT compatibility, compilation plus trim analysis, passed cleanly.)

Known limitations

  • ForMember overrides on destination members that are only settable via a constructor (a record's primary-constructor parameters) are recorded but have no effect - the override doesn't reroute into the constructor argument. Common mutable-property DTOs (including init/required/private setters) are unaffected.
  • Include/IncludeBase/IncludeAllDerived merge configuration one level deep and do not dispatch to a runtime-derived type from a base-typed reference (no polymorphic Map<Animal> dispatch yet).
  • Condition and PreCondition are currently equivalent (both gate whether a member is assigned); AutoMapper's distinction between "before" and "after" value resolution isn't modeled yet.
  • Dynamic/dictionary-sourced mapping (ExpandoObject, Dictionary<string, object>) is the one place that inspects a value's runtime type; nested collections inside a dynamic object are boxed as-is rather than recursively converted to nested lists of dictionaries.
  • Open-generic CreateMap registrations carry no member customization and aren't validated by AssertConfigurationIsValid.
  • Reference-identity preservation for a non-cyclic diamond share (the same instance reachable from two different, non-self-referencing branches of a graph) is no longer guaranteed unless the shared type is itself self-referential - see the Performance section below.
  • No incremental Roslyn source generator yet (deferred rather than shipped half-working - see ROADMAP.md); no Blazor sample yet.

Roadmap

See ROADMAP.md for the full phase-by-phase breakdown, what's still open in Phase 4 (source generator, remaining samples, continued performance work), and ideas beyond Phase 4.

Further reading

  • docs/Architecture.md - how the pieces fit together, why the project is split into two assemblies, and how ProjectTo differs from the runtime engine.
  • docs/Migration.md - a side-by-side table for AutoMapper/Mapster users.
  • docs/Performance.md - what's free, what costs something, and how to benchmark your own mappings.
  • docs/BestPractices.md - configuration, ForMember, ProjectTo vs Map, and testing recommendations.
  • CONTRIBUTING.md - project layout and the engine's non-negotiable properties (no reflection while mapping, thread safety, pure-expression-tree ProjectTo).

Building, testing & benchmarking

dotnet build
dotnet test
dotnet run -c Release --project tests/MeliorMapper.Benchmarks

Performance status (honest numbers)

Benchmark: mapping a Customer (nested Address, a List<Order> of 10 items) to CustomerDto, .NET 10, BenchmarkDotNet, [MemoryDiagnoser].

Before the optimization pass below:

Method Mean Ratio Allocated Alloc Ratio
ManualMapping 163.6 ns 1.00 752 B 1.00
Mapster 166.4 ns 1.06 680 B 0.90
AutoMapper 273.8 ns 1.75 872 B 1.16
MeliorMapper 869.6 ns 5.55 1744 B 2.32

MeliorMapper was slower than AutoMapper and Mapster, and allocated over 2x manual mapping. Root cause: every class-to-class map unconditionally paid for circular-reference protection (a Dictionary<object,object> allocated per top-level Map call, plus a lookup before constructing every nested object, even ones like Address/Order that can never cycle back to themselves), and nested member conversions were routed through a ConcurrentDictionary<TypePair, Lazy<T>> lookup on every call instead of a direct reference.

After two targeted fixes - (1) a compile-time structural reachability check (TypeGraphAnalyzer.IsSelfReachable) that only enables the reference cache for type maps that can actually reach themselves again (real self-references like Node/NodeDto's Parent/Children keep full protection; everything else skips the allocation entirely), and (2) dropping the Lazy<T> wrapper from the delegate caches (a plain ConcurrentDictionary<TypePair, MapDelegate>, accepting rare benign duplicate compilation under first-access contention instead of paying a volatility check on every call):

Method Mean Ratio Allocated Alloc Ratio
ManualMapping 142.6 ns 1.02 752 B 1.00
Mapster 200.5 ns 1.43 680 B 0.90
AutoMapper 353.0 ns 2.52 872 B 1.16
MeliorMapper 456.4 ns 3.26 752 B 1.00

Mean time dropped ~47% (869.6 ns → 456.4 ns) and allocations now exactly match manual mapping (752 B, down from 1744 B) - the reference-cache allocation is gone for this non-self-referencing DTO shape. AutoMapper's own number moved between runs too (274 ns → 353 ns), which is ordinary benchmark noise at this scale on a shared machine - treat the ratios as directional, not exact.

A third fix removed the remaining overhead called out above: nested member/constructor-argument/ collection-element/dictionary-key-value conversions used to go through engine.MapCore(...) at map time - a ConcurrentDictionary<TypePair, MapDelegate> lookup on every single call, even though the delegate for that type pair was already known once the parent type map finished compiling. CompilationSession (new) now resolves and embeds that nested delegate directly as a compiled-in constant while the parent's expression tree is being built, so map-time nested conversions are a direct delegate invocation, not a dictionary hit. Self-referencing type graphs (Node/NodeDto's Parent/Children) are still compiled safely: if a type pair is already being compiled further up the same call stack, CompilationSession embeds a one-time indirection through a DelegateBox (a mutable cell filled in once the enclosing compile finishes) instead of recursing into the compiler again and overflowing the stack.

Method Mean Ratio Allocated Alloc Ratio
Mapster 115.1 ns 0.89 680 B 0.90
ManualMapping 129.6 ns 1.00 752 B 1.00
MeliorMapper 182.1 ns 1.41 752 B 1.00
AutoMapper 222.8 ns 1.73 872 B 1.16

Mean time dropped a further ~60% (456.4 ns → 182.1 ns) with allocations unchanged (still 752 B, matching manual mapping exactly). MeliorMapper is now faster than AutoMapper and within reach of Mapster/manual mapping - the remaining gap is inherent to the two mechanisms manual/Mapster-style mapping doesn't pay for at all: ResolutionContext allocation per top-level Map call and the delegate-Invoke indirection itself (a real function-pointer call per nested member, vs. Mapster's fully inlined single expression tree for the whole graph).

One documented trade-off from the first fix above: reference-identity preservation for a non-cyclic diamond share (the same object instance reachable from two different, non-self-referencing branches of a graph) is no longer guaranteed unless the shared type is itself self-referential. Preventing actual stack overflows on real cycles (the correctness-critical case) still works exactly as before, and is covered by tests - only the narrower "same non-recursive object appears twice, are the two mapped copies still reference-equal" guarantee is now scoped to self-referencing types.

Product Compatible and additional computed target framework versions.
.NET 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 is compatible.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on MeliorMapper:

Package Downloads
MeliorMapper.DependencyInjection

Microsoft.Extensions.DependencyInjection integration for MeliorMapper.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 176 7/27/2026