FusionMapper 1.0.0
dotnet add package FusionMapper --version 1.0.0
NuGet\Install-Package FusionMapper -Version 1.0.0
<PackageReference Include="FusionMapper" Version="1.0.0" />
<PackageVersion Include="FusionMapper" Version="1.0.0" />
<PackageReference Include="FusionMapper" />
paket add FusionMapper --version 1.0.0
#r "nuget: FusionMapper, 1.0.0"
#:package FusionMapper@1.0.0
#addin nuget:?package=FusionMapper&version=1.0.0
#tool nuget:?package=FusionMapper&version=1.0.0
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:
- Arrays &
List<T>: Generates standardEnumerable.ToArray()orEnumerable.ToList(). - Interfaces (
IEnumerable<T>,IReadOnlyList<T>, etc.): Generates modern C# Collection Expressions ([.. items]) for zero-overhead materialization and minimal allocations. - Custom Collections:
- Constructor Injection: If it accepts
IEnumerable<T>, generatesnew CustomCollection(items). - AddRange: If it has a parameterless constructor and
AddRange, generates an optimized initialization block. - Add Loop: Falls back to a highly optimized
foreachloop withAdd().
- Constructor Injection: If it accepts
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:
- Calls
Clear()on the existing collection. - Adds the newly mapped items using
AddRange()or aforeachloop withAdd(). - Arrays: Because arrays cannot be resized, array properties are skipped or replaced during in-place mutation.
- Identity Optimization: If the source and target element types are identical, it checks
ReferenceEqualsto 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:
Rename the target member or the source member so the conventions match.
[FusionMapperIgnore]— mark members that are intentionally not mapped:public class ProductDto { public int AvailableStock { get; init; } [FusionMapperIgnore] public ProductColor ProductColor { get; init; } }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 | Versions 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. |
-
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 |