FusionMapper 1.0.0

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

FusionMapper

FusionMapper is a modern, high-performance object mapping library for .NET. It combines the developer-friendly, zero-configuration convention-based approach of AutoMapper with the compile-time code generation and zero-overhead performance of Mapperly and Mapster.

Built for .NET 8/9/10 and C# 12/13/14, it leverages Source Generators and C# Interceptors to eliminate runtime reflection, boilerplate configuration, and mapping overhead.


📦 1. Installation

Install FusionMapper via NuGet:

Package Manager:

Install-Package FusionMapper -AllowPrereleaseVersions

.NET CLI:

dotnet add package FusionMapper --prerelease 

Prerequisites: FusionMapper uses Source Generators and C# Interceptors. Ensure your project targets at least .NET 8.0 and uses C# 12 or higher. Interceptors are fully utilized on .NET 9+.


🚀 2. Basic Usage

FusionMapper requires zero configuration. No profiles, no CreateMap, no manual setup. Just use the fluent extension methods.

Create a new object

var source = new User { FirstName = "Alice", LastName = "Smith", Age = 30 };

// Maps to a new instance of UserDto
var dto = source.Map().To<UserDto>(); 

Map to an existing object

var existingDto = new UserDto { Id = 1 };

// Updates the existing instance in-place
source.Map().To(existingDto); 

Project IQueryable (EF Core / LINQ to SQL)

// Translates directly to SQL via Expression Trees
var dtos = dbContext.Users
    .Project()
    .To<UserDto>()
    .ToList();

✅ 3. Supported Scenarios & Code Examples

FusionMapper relies on Convention over Configuration. It automatically maps properties and fields by name, handles flattening, respects nullability annotations, and supports complex scenarios out of the box.

3.1. Properties & Fields

Maps public instance properties and fields by exact name, case-insensitive match, or ignoring leading underscores.

public class Source { public string Name { get; set; } = ""; public int _Value { get; set; } }
public class Target { public string name { get; set; } = ""; public int Value { get; set; } }

var target = source.Map().To<Target>(); 
// Maps Name -> name, _Value -> Value

3.2. Flattening & Deep Flattening

Automatically flattens nested objects by concatenating property names.

public class Order { public Customer Customer { get; set; } = new(); }
public class Customer { public Address Address { get; set; } = new(); }
public class Address { public string City { get; set; } = ""; }

public class OrderDto { public string CustomerAddressCity { get; set; } = ""; }

var dto = order.Map().To<OrderDto>();
// dto.CustomerAddressCity == order.Customer.Address.City

3.3. Nullable Reference Types (NRT) & Null-Safety 🌟

FusionMapper fully understands C# Nullable Reference Types. It analyzes nullability annotations at compile-time and generates safe null-checks, preventing unexpected NullReferenceExceptions during flattening.

Flattening through nullable intermediate objects:

public class Order { public Customer? Customer { get; set; } } // Nullable intermediate
public class Customer { public string Name { get; set; } = ""; }

public class OrderDto { public string? CustomerName { get; set; } } // Nullable target

// Generated code safely handles the null intermediate:
// dto.CustomerName = source.Customer == null ? null : source.Customer.Name;

Nullable to Non-Nullable (and vice versa):

public class Source {
    public string? Name1 { get; set; }
    public string Name2 { get; set; } = "";
}

public class Target {
    public string Name1 { get; set; } = ""; // Throws InvalidOperationException if source is null
    public string? Name2 { get; set; }         // Safely accepts null
}

3.4. Collections & Materialization 📦

FusionMapper supports a wide variety of collection types. Unlike runtime mappers that use reflection to find Add methods, FusionMapper's Source Generator analyzes your target collection at compile time and emits the most efficient, direct initialization code.

Supported Target Types:

  • Arrays: T[]
  • Lists: List<T>
  • Interfaces: IEnumerable<T>, IList<T>, IReadOnlyList<T>, ICollection<T>
  • Custom Collections: Any type implementing IEnumerable<T>.
🏗 How the Source Generator Optimizes Collections

The generator inspects the target type and selects the optimal creation strategy:

  1. Arrays & List<T>: Generates standard Enumerable.ToArray() or Enumerable.ToList().
  2. Interfaces (IEnumerable<T>, IReadOnlyList<T>, etc.): Generates modern C# Collection Expressions ([.. items]) for zero-overhead materialization and minimal allocations.
  3. Custom Collections:
    • Constructor Injection: If it accepts IEnumerable<T>, generates new CustomCollection(items).
    • AddRange: If it has a parameterless constructor and AddRange, generates an optimized initialization block.
    • Add Loop: Falls back to a highly optimized foreach loop with Add().
Example: Mapping to Interfaces & Custom Collections
public class Source { public List<Item> Items { get; set; } = []; }

public class Target { 
    public IReadOnlyList<ItemDto> ReadOnlyItems { get; set; } = [];
    public CustomItemCollection CustomItems { get; set; } = new();
}
What the Source Generator actually emits
// For IReadOnlyList<T> (Uses C# 12 Collection Expressions for max performance)
target.ReadOnlyItems = [.. global::System.Linq.Enumerable.Select(source.Items, static __item => new ItemDto { Name = __item.Name })];

// For Custom Collections with IEnumerable<T> constructor
target.CustomItems = new CustomItemCollection(global::System.Linq.Enumerable.Select(source.Items, static __item => new ItemDto { Name = __item.Name }));

// For Custom Collections with AddRange
var __mapped = global::System.Linq.Enumerable.ToList(source.Items.Select(...));
var __result = new CustomCollection();
__result.AddRange(__mapped);
target.CustomItems = __result;

3.5. In-Place Collection Mutation (Existing Objects) 🔄

When mapping to an existing object, FusionMapper does not just replace the collection reference. It intelligently mutates the existing collection in-place to preserve object identity (crucial for UI frameworks like WPF/MAUI or EF Core tracking).

Behavior:

  1. Calls Clear() on the existing collection.
  2. Adds the newly mapped items using AddRange() or a foreach loop with Add().
  3. Arrays: Because arrays cannot be resized, array properties are skipped or replaced during in-place mutation.
  4. Identity Optimization: If the source and target element types are identical, it checks ReferenceEquals to skip unnecessary clearing and adding.
public class Source { public List<Item> Items { get; set; } = []; }

public class Target { 
    // Read-only collection property
    public List<ItemDto> Items { get; } = []; 
}

var target = new Target();
target.Items.Add(new ItemDto { Name = "Old" });

var source = new Source { Items = [new() { Name = "New" }] };
source.Map().To(target);

// Generated code under the hood:
// var __mappedItems = source.Items.Select(i => new ItemDto { Name = i.Name }).ToList();
// target.Items.Clear();
// target.Items.AddRange(__mappedItems);
// (The reference to target.Items remains exactly the same!)

3.6. Aggregates (Killer Feature) 📊

Performs collection operations purely through target property naming conventions. Supports Count, Any, All, Sum, Average, Max, Min, First, Last, FirstOrDefault, LastOrDefault.

public class Order { public List<OrderLine> Lines { get; set; } = []; }
public class OrderLine { public decimal Amount { get; set; } public bool IsActive { get; set; } }

public class OrderDto {
    public int LinesCount { get; set; }                     // Lines.Count()
    public bool LinesIsActiveAny { get; set; }              // Lines.Any(x => x.IsActive)
    public decimal LinesAmountSum { get; set; }             // Lines.Sum(x => x.Amount)
    public string? LinesNameFirstOrDefault { get; set; }    // Lines.Select(x => x.Name).FirstOrDefault()
}

3.7. Constructors, required & init

Automatically selects the best constructor and maps parameters by name. Fully supports C# 11+ required and init members.

public class Target {
    public required string Name { get; init; }
    public int Age { get; }
    public Target(string name, int age) { Name = name; Age = age; }
}
// Generates: new Target(source.Name, source.Age) { Name = source.Name }

An init-only or required member that cannot be filled from the source is reported as FMAP001 at build time (a compile error when interceptors are active, otherwise a warning) and throws MappingException at runtime, listing the members that could not be assigned (see §5).

3.8. Type Conversions & Nullable Value Types

Handles implicit/explicit casts, Enum ↔ String, Enum ↔ Int, and safely unwraps/wraps Nullable<T>.

public enum Status { Active, Inactive }
public class Source { public Status Status { get; set; } public int? IntValue { get; set; } }
public class Target { public string Status { get; set; } = ""; public int IntValue { get; set; } }

// Enum -> String, Nullable<int> -> int (throws InvalidOperationException if source is null)

❌ 4. Unsupported Scenarios (By Design)

Feature Reason
Manual Configuration No MapFrom, Ignore, or ConvertUsing. Everything is strictly convention-based.
Cyclic / Recursive Graphs To prevent infinite loops and stack overflows at compile/runtime.
Runtime Polymorphism Maps based on static compile-time types, not runtime GetType().
Anonymous Types Source generators cannot reliably map from/to anonymous types.

📊 5. Source Generator Diagnostics

FusionMapper validates your mappings at compile time and reports diagnostics directly in your IDE via Roslyn diagnostics.

Code Severity Description
FMAP001 Error¹ / Warning¹ Cannot generate mapping. Reported when types are incompatible or no suitable constructor is found.
FMAP002 Error¹ / Warning¹ Unsupported mapping inside expression tree. Reported when trying to map to an existing object (e.g., Map().To(existing)) inside an IQueryable projection.
FMAP003 Warning Anonymous source/target type. Reported when the source or target type is an anonymous type.
FMAP004 Warning Cannot resolve backing field for the UnsafeAccessor fast path; a fallback field name is used.
FMAP005 Warning Target members have no matching source members. Reported when a settable target member cannot be filled from the source and would silently keep its default value (0, null, ...). Lists all affected members in a single diagnostic per call site.

¹ FMAP001/FMAP002 severity depends on who owns the call. When the interceptor path is active (.NET 9+ by default, or unless disabled — see §6), the generator generates the mapping, so an impossible one is a compile error. When the call is resolved by the runtime fallback (interceptors disabled via EnableFusionMapperInterceptor, or on .NET 8), it is only a warning — the call compiles and fails fast at runtime with MappingException.

Unmapped target members (FMAP005)

Since FusionMapper is strictly convention-based, a target member whose name does not match anything in the source graph would previously be silently skipped — the mapping compiles and runs, but the member keeps its default value. FMAP005 turns this into a visible warning:

warning FMAP005: The following members of 'ProductDto' have no matching source members and will keep
their default values: AvailableStock, RestockThreshold, ProductColor.

Members assigned through a constructor (e.g., positional records) are never reported.

Ways to resolve / suppress:

  1. Rename the target member or the source member so the conventions match.

  2. [FusionMapperIgnore] — mark members that are intentionally not mapped:

    public class ProductDto
    {
        public int AvailableStock { get; init; }
    
        [FusionMapperIgnore]
        public ProductColor ProductColor { get; init; }
    }
    
  3. FusionMapperSuppressUnmappedWarnings — suppress FMAP005 for the whole project:

    <PropertyGroup>
      <FusionMapperSuppressUnmappedWarnings>true</FusionMapperSuppressUnmappedWarnings>
    </PropertyGroup>
    

When a mapping cannot be generated

When the runtime fallback owns the call (interceptors disabled via EnableFusionMapperInterceptor, or on .NET 8), FMAP001/FMAP002 are warnings: the call site compiles and fails fast at runtime with MappingException. With the interceptor path active (.NET 9+ by default) the same diagnostics are compile-time errors.

// 'Title' cannot reach the required 'Name' member:
var dto = source.Map().To<RequiredTarget>();
// Throws MappingException: Required members of type 'RequiredTarget' is not mapped: 'Name'.

Runtime rules for edge cases:

Scenario Behavior
Creation (To<T>() or To<T>(null)) with no matching source members Target is created with default member values (FMAP005 warns about them at build time).
Creation from a collection source into a non-collection target MappingException — the whole source payload would be silently dropped.
Mapping into an existing object where nothing matches MappingException (Nothing were mapped from ...).
Map().To(existing) inside an IQueryable projection MappingException (see FMAP002).
Cyclic / recursive graphs MappingException (see §4).

🏗 6. What Code Does It Generate?

Because FusionMapper uses Source Generators and C# Interceptors, it generates highly optimized, readable C# code at build time. On .NET 9+ the interceptor path has zero runtime reflection.

On .NET 8 (or when interceptors are explicitly disabled) generated mappers are pre-registered at startup via a ModuleInitializer, and only mappings the generator could not produce fall back to a runtime expression builder. You can disable interceptors explicitly:

<PropertyGroup>
  <EnableFusionMapperInterceptor>false</EnableFusionMapperInterceptor>
</PropertyGroup>

When you write:

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

The Source Generator intercepts the call and generates the following code behind the scenes:

1. The Mapper Method (Zero-overhead logic)

// <auto-generated />
#nullable enable
namespace FusionMapper;

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("FusionMapper", "1.0.0.0")]
static class Generated
{
    [global::System.Runtime.CompilerServices.MethodImplAttribute(
        global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining | 
        global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)]
    public static UserDto Map__User__To__UserDto(User source)
    {
        if (source == null) return default;
        return new UserDto() 
        { 
            Name = source.Name, 
            Age = source.Age 
        };
    }
}

2. The Interceptor (Rewrites your call, .NET 9+)

namespace System.Runtime.CompilerServices
{
    sealed file class InterceptsLocationAttribute : Attribute { /* ... */ }
}

namespace FusionMapper
{
    static file class Interceptors
    {
        [global::System.Runtime.CompilerServices.InterceptsLocation(1, "base64_encoded_location_data")]
        public static UserDto To(this in global::FusionMapper.FusionSource<User> receiver)
        {
            ref User source = ref SourceAccessor<User>.GetValue(in receiver);
            return global::FusionMapper.Generated.Map__User__To__UserDto(source);
        }
    }
}

3. The Initializer (Pre-warms caches for Expression Trees)

For IQueryable projections, it registers expression trees at startup using ModuleInitializer and UnsafeAccessor (on .NET 9+):

static file class Initializer
{
    [global::System.Runtime.CompilerServices.ModuleInitializer]
    internal static void Initialize()
    {
        var cache = GetCache(null!);
        cache.TryAdd((typeof(User), typeof(UserDto)), global::FusionMapper.Generated.Project__User__To__UserDto);
    }
}

🤝 Contributing & License

FusionMapper is open-source and licensed under the MIT License. Contributions, bug reports, and feature requests are welcome!

Built with ❤️ using Qwen, .NET 10, C# 14, and tested with TUnit.

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

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 86 9/19/2026