SourceCrafter.Mappify
1.0.0
dotnet add package SourceCrafter.Mappify --version 1.0.0
NuGet\Install-Package SourceCrafter.Mappify -Version 1.0.0
<PackageReference Include="SourceCrafter.Mappify" Version="1.0.0" />
<PackageVersion Include="SourceCrafter.Mappify" Version="1.0.0" />
<PackageReference Include="SourceCrafter.Mappify" />
paket add SourceCrafter.Mappify --version 1.0.0
#r "nuget: SourceCrafter.Mappify, 1.0.0"
#:package SourceCrafter.Mappify@1.0.0
#addin nuget:?package=SourceCrafter.Mappify&version=1.0.0
#tool nuget:?package=SourceCrafter.Mappify&version=1.0.0
SourceCrafter.Mappify
A Roslyn incremental source generator for .NET that produces type-mapping extension methods at compile-time — zero runtime overhead, no reflection.
Table of Contents
- Features
- Installation
- Quick Start
- Declaring Mappings
- Attributes Reference
- Generated Methods
- Member Matching Rules
- Type Conversion
- Collection Types Supported
- Enum Extensions
- UnsafeAccessor Support
- Interface Mapping
- MappingKind
- ApplyTo
- Advanced Scenarios
- Generated Output Example
Features
| Feature | Description |
|---|---|
| Compile-time generation | All mapper code is emitted at build time as partial extension methods — no runtime reflection |
| Bidirectional mapping | A single attribute declaration generates both ToTarget() and ToSource() |
| In-place update | Update(source) reuses the existing target instance |
| Deep copy | Copy() performs a full structural copy |
| Collection mapping | Arrays, List<T>, Dictionary, Span<T>, Stack<T>, Queue<T>, ReadOnlyCollection<T>, and more |
| Tuple ↔ object | Value tuples map automatically to/from named types |
| UnsafeAccessor | Read-only fields and init-only properties are assigned via UnsafeAccessorAttribute on .NET 8+ |
| Recursive types | Self-referential types are supported with a configurable depth limit |
| Enum extensions | Rich enum helpers (names, values, descriptions, lookup) via [Extend] |
| Null-safety | Full nullable-aware code generation |
Installation
Reference the generator package so it runs during compilation:
dotnet package add SourceCrafter.Mappify
Quick Start
// AssemblyInfo.cs (or any file)
using SourceCrafter.Mappify.Attributes;
using MyApp.Models;
[assembly: Map<User, UserDto>]
That's it. The generator immediately produces the following extension methods inside SourceCrafter.Mappify.Mappings:
// Created → new target populated from source
UserDto dto = user.ToUserDto();
// Update → existing target populated from source (returns target)
dto.Update(user);
// Copy → deep structural copy of the same type
User copy = user.Copy();
Declaring Mappings
Assembly-level
Use [assembly: Map<TSource, TTarget>] in any file to register a bidirectional mapping. The MappingKind and ApplyTo parameters are optional.
[assembly:
Map<User, UserDto>,
Map<IAppUser, MeAsUser>,
Map<User, UserMiniDto>(MappingKind.Normal) // only ToXxx, no Update
]
Type-level
Decorate a class or struct directly with [Map<TSource>]:
[Map<User>]
public class UserMiniDto
{
public string FullName { get; set; } = null!;
public int Count { get; set; }
public int Age { get; set; }
}
Attributes Reference
MapAttribute
Assembly / type level — registers a bidirectional mapping.
// Generic two-type form (assembly attribute)
[assembly: Map<TSource, TTarget>(
kind = MappingKind.All, // Normal | Fill | All
ignore = ApplyTo.None, // suppress a side
ignoreMembers = new[]{ "Secret" } // member name blacklist
)]
// Generic one-type form (on a class/struct)
[Map<TSource>(MappingKind.Fill)]
public class MyDto { ... }
Member level — manually binds this member to a named member on another type.
// Map Balance (on User) → TotalAmount (on UserDto)
[Map(nameof(UserDto.TotalAmount))]
public double? Balance { get; set; }
// Apply only when User is the source
[Map(nameof(User.FullName), ApplyTo.Source)]
public string Name { get; set; }
IgnoreAttribute
Excludes a member from all mappings in both directions.
[Ignore]
public string? InternalSecret { get; set; }
IgnoreForAttribute
Excludes a member only when mapping against a specific counterpart member.
[IgnoreFor(nameof(UserDto.Supervisor))]
public User? Supervisor { get; init; }
Use the ApplyTo parameter to restrict which direction the exclusion applies:
[IgnoreFor(nameof(UserDto.Supervisor), ApplyTo.Source)]
public User? Supervisor { get; init; }
MaxAttribute
Caps the recursion depth for self-referential members.
[Max(2)]
public Role MainRole { get; set; }
ExtendAttribute
Generates enum extension helpers.
// On an enum declaration
[Extend]
public enum Status { NotStarted, Started, Stopped, Cancelled, Failed }
// As an assembly attribute (for types you don't own)
[assembly: Extend<MappingKind>]
Generated Methods
Object Mapping
For every registered Map<TSource, TTarget> pair the generator emits the methods below inside public static partial class SourceCrafter.Mappify.Mappings.
| Method | Signature | Description |
|---|---|---|
| Convert | TTarget ToTarget(this TSource source) |
Creates a new TTarget populated from source |
| Update | TTarget Update(this TTarget target, TSource source) |
Updates target in-place; returns target |
| Copy | TSelf Copy(this TSelf source) |
Structural copy (same-type mapping) |
All three methods are generated by default (MappingKind.All).
Use MappingKind.Normal to generate only ToXxx / Copy, or MappingKind.Fill for only Update.
Collection Mapping
For every pair of compatible collection types:
| Method | Description |
|---|---|
ToTargetCollection(this TSourceCol source) |
Creates a new target collection |
Update(this TTargetCol target, TSourceCol source) |
Clears and refills target from source |
Member Matching Rules
The generator matches members automatically using the following priority:
- Manual binding —
[Map(nameof(Other.Member))]on either side - Exact name —
Balance↔Balance - Type-prefixed name —
UserBalanceon source matchesBalanceon a type namedUser - Case-insensitive — when at least one side is a value tuple
- Key/Value semantics — tuple elements
id/itemmap toKey/ValueinKeyValuePairandDictionaryitems
Matching respects [Ignore] and [IgnoreFor] before any assignment is emitted.
Type Conversion
The generator selects a conversion strategy per member pair:
| Strategy | When used |
|---|---|
| Direct assignment | Types are assignment-compatible or identical |
| Explicit cast | A user-defined explicit operator exists |
| Implicit cast | A user-defined implicit operator exists |
| Recursive mapper | A nested complex type is also registered for mapping |
| Collection mapper | Both sides are supported collection types |
Nullability is respected throughout — a nullable source assigned to a non-nullable target gets ?? default! or a null-guard as appropriate.
Collection Types Supported
| .NET Type | Notes |
|---|---|
T[] |
Array with index-based for loop and Array.Resize for unknown-length sources |
List<T> / IList<T> / ICollection<T> |
Standard Add append |
IEnumerable<T> |
Forward-only enumeration |
ReadOnlyCollection<T> / IReadOnlyCollection<T> / IReadOnlyList<T> |
Wrapped List<T> internally |
Stack<T> |
Push append |
Queue<T> |
Enqueue append |
Span<T> |
Ref-like; used as method parameter |
ReadOnlySpan<T> |
Read-only ref-like |
Dictionary<TKey,TValue> / IDictionary<TKey,TValue> |
Key/Value mapping; also maps from List<(id, item)> |
Cross-collection-type mapping is fully supported, e.g. List<(string id, string item)> → Dictionary<object, string>.
Enum Extensions
Decorate an enum with [Extend] (or use [assembly: Extend<TEnum>] for external enums) to generate the following members via C# 14 extension blocks:
[Extend]
[Flags]
public enum Status
{
NotStarted,
[Description("Transaction was stopped")] Stopped,
[Description("Transaction has been started")] Started,
[Description("Transaction has been cancelled by user")] Cancelled,
[Description("Transaction had an external failure")] Failed
}
Generated API
// Static collections
ReadOnlySpan<Status> allValues = Status.Values;
ReadOnlySpan<string> allNames = Status.Names;
ReadOnlySpan<string> allDescs = Status.Descriptions;
// Instance helpers
string? name = Status.Started.Name; // "Started"
string? desc = Status.Cancelled.Description; // "Transaction has been cancelled by user"
// Lookup
bool found = Status.TryGetValue("Cancelled", out Status s); // true
bool named = Status.Started.TryGetName(out string? n); // true, "Started"
bool descd = Status.Cancelled.TryGetDescription(out string? d); // true
// Existence checks
bool byInt = Status.IsDefined(1); // true
bool byName = Status.IsDefined("Failed"); // true
bool bad = Status.IsDefined(99); // false
UnsafeAccessor Support
On .NET 8+, the generator automatically detects UnsafeAccessorAttribute availability and uses it to assign:
readonlyfieldsinit-only properties (via their backing field)- Nullable value-type backing fields
The accessor helper methods are emitted into a separate MappingExtras file and are not part of the public API.
// Example of a generated unsafe accessor
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "<Id>k__BackingField")]
extern static ref int? GetId(this UserDto _);
Interface Mapping
To map to an interface while producing a concrete implementation, use the IImplement<IInterface, Implementation> marker:
// TypeSet will treat IAppUser as the shape but instantiate MeAsUser
[assembly: Map<IImplement<IAppUser, MeAsUser>, User>]
MappingKind
Controls which methods are generated for a mapping pair.
| Value | int | Methods generated |
|---|---|---|
Normal |
1 | ToXxx() / Copy() |
Fill |
2 | Update() |
All |
3 | Both (default) |
[assembly: Map<User, UserDto>(MappingKind.Normal)] // no Update
[assembly: Map<User, UserDto>(MappingKind.Fill)] // no ToUserDto/ToUser
ApplyTo
Scopes an attribute to one side of the mapping relationship.
| Value | Meaning |
|---|---|
None |
No side suppressed (default) |
Source |
Applies when this type/member is the source |
Target |
Applies when this type/member is the target |
Both |
Applies in both roles |
Used on [Map], [IgnoreFor], and [Ignore] at the member level:
// WindowsUser.Name maps to User.FullName only when WindowsUser is the source
[Map(nameof(User.FullName), ApplyTo.Source)]
public string Name { get; set; }
Advanced Scenarios
Recursive / Self-referential Types
Use [Max(n)] on the recursive member to cap depth. The generated code receives depth and maxDepth parameters with a guard at the top:
public class User
{
[Max(2)]
public User? Supervisor { get; init; }
}
Tuple ↔ Struct mapping
Value tuples map naturally to structs by field-name/position rules:
// UserDto.MainRole is (int id, string name)
// User.MainRole is Role { int Id; string Name; }
// Generated: source.MainRole.ToRole() / role.ToTupleOfIntAndString()
Numeric type widening / narrowing
// User.Balance is double?
// UserDto.TotalAmount is decimal
// Generated cast: target.TotalAmount = (decimal)(source.Balance ?? default!);
Dictionary ↔ List of tuples
// User.ExtendedProperties is Dictionary<object, string>
// UserDto.ExtendedProperties is List<(string id, string item)>
// Generated: source.ExtendedProperties.ToDictionaryOfObjectAndString()
// source.ExtendedProperties.ToListOfTupleOfStringAndString()
Generated Output Example
Given:
[assembly: Map<User, UserDto>]
The generator emits 021_UserDto_User.map.g.cs:
#nullable enable
namespace SourceCrafter.Mappify;
public static partial class Mappings
{
// ── UserDto self-copy ─────────────────────────────────────────────
public static global::SourceCrafter.UnitTests.UserDto Copy(
this global::SourceCrafter.UnitTests.UserDto source)
=> Update(new global::SourceCrafter.UnitTests.UserDto(), source);
public static global::SourceCrafter.UnitTests.UserDto Update(
this global::SourceCrafter.UnitTests.UserDto target,
global::SourceCrafter.UnitTests.UserDto source, int __l = 0)
{
target.GetId() = source.Id;
target.FullName = source.FullName;
target.Age = source.Age;
target.DateOfBirth = source.DateOfBirth;
target.TotalAmount = source.TotalAmount;
target.GetMainRole().Update(source.MainRole);
target.ExtendedProperties.Update(source.ExtendedProperties);
// ... remaining members
return target;
}
// ── UserDto → User ────────────────────────────────────────────────
public static global::SourceCrafter.UnitTests.User ToUser(
this global::SourceCrafter.UnitTests.UserDto source)
=> Update(new global::SourceCrafter.UnitTests.User(), source);
public static global::SourceCrafter.UnitTests.User Update(
this global::SourceCrafter.UnitTests.User target,
global::SourceCrafter.UnitTests.UserDto source, int __l = 0)
{
target.GetId() = source.Id;
target.FullName = source.FullName;
target.Age = source.Age;
target.Balance = (double)source.TotalAmount; // decimal → double
target.GetMainRole().Update(source.MainRole); // tuple → struct
target.GetExtendedProperties()
.Update(source.ExtendedProperties); // List<tuple> → Dictionary
// ...
return target;
}
// ── User → UserDto ────────────────────────────────────────────────
public static global::SourceCrafter.UnitTests.UserDto ToUserDto(
this global::SourceCrafter.UnitTests.User source)
=> Update(new global::SourceCrafter.UnitTests.UserDto(), source);
public static global::SourceCrafter.UnitTests.UserDto Update(
this global::SourceCrafter.UnitTests.UserDto target,
global::SourceCrafter.UnitTests.User source, int __l = 0)
{
target.GetId() = source.Id ?? default!;
target.FullName = source.FullName;
target.TotalAmount = (decimal)(source.Balance ?? default!); // double → decimal
target.GetMainRole().Update(source.MainRole);
// ...
return target;
}
}
License
MIT © SourceCrafter
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. 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 was computed. 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 was computed. 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 | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.CodeAnalysis.Common (>= 4.14.0)
- Microsoft.CodeAnalysis.CSharp (>= 4.14.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 | 148 | 4/13/2026 |