Mappa.Generator 10.3.0

dotnet add package Mappa.Generator --version 10.3.0
                    
NuGet\Install-Package Mappa.Generator -Version 10.3.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="Mappa.Generator" Version="10.3.0">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Mappa.Generator" Version="10.3.0" />
                    
Directory.Packages.props
<PackageReference Include="Mappa.Generator">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
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 Mappa.Generator --version 10.3.0
                    
#r "nuget: Mappa.Generator, 10.3.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 Mappa.Generator@10.3.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=Mappa.Generator&version=10.3.0
                    
Install as a Cake Addin
#tool nuget:?package=Mappa.Generator&version=10.3.0
                    
Install as a Cake Tool

Mappa.Generator

This source generator generates code for partial methods in partial classes tagged with the [Mappa] attribute defined in the Mappa package. The generated code allow to map from a source type to a target type.

Assuming you have a partial method like the following

[Mappa]
public partial class Mapper
{
    public partial TTarget Map(TSource input);
}

where TSource is the source type of the mapping and TTarget is the target type of the mapping, the source generator works by applying the strategies described below (see TypeMapIdentifierAlgorithm.cs).

Incremental discovery

The mapper and dependency-injection pipelines use Roslyn ForAttributeWithMetadataName so only types that declare [Mappa] or [MappaDependencyInjection] are considered. Those provider predicates also require a partial ClassDeclarationSyntax. Non-partial attributed classes are ignored by those syntax providers (no generation and no provider-driven diagnostics). See Documentation/mappa-generator-algorithm.md — Incremental discovery and CancellationToken convention.

A third pipeline discovers [MappaInterceptor] classes without requiring partial in the syntax predicate, so non-partial (and static) classes can receive diagnostics (MP00119, MP00120). Valid partial, non-static interceptor classes receive instance MappaMap (or renamed) stubs and per–call-site interceptor methods built with the shared mapping algorithm after collating trailing With configuration (new arguments only). Consumer projects must list mapper namespaces in MappaInterceptorsNamespaces (merged into InterceptorsNamespaces via the package build/buildTransitive props). See Interceptor inline mapping and MappaInterceptor.

Dependency injection registration

Partial classes marked with [MappaDependencyInjection] are handled by a second incremental pipeline that emits an IServiceCollection registration method for [Mappa] mapper types in the registrar's assembly and, when InjectFromAssemblies is set, in those marker assemblies as well. Non-partial registrars never enter this pipeline. See Documentation/mappa-generator-algorithm.md — Dependency injection registration and Mappa attributes — MappaDependencyInjection.

The methods that can be auto-generated by 🗺️ Mappa need to satisfy the following:

  • The class they belong to needs to be marked with the [Mappa] attribute;
  • The class they belong to needs to be partial;
  • The method needs to be partial;
  • The method must return a non-void type;
  • The method must not return a Task<T> (i.e. mapper methods cannot be async);
  • The method must have one or two parameters; if the method has two parameters, the second must be of type Mappa.MappaContext;
  • Methods marked with [MappaIgnore] are excluded from mapping resolution;
  • Non-partial methods with an existing implementation can be registered as pre-mapped (used when resolving nested mappings);
  • Methods from the mapper base-class hierarchy are collected automatically;
  • Parameter ref modifiers must be absent or in; any other ref modifier causes the method to be rejected.

Strategy resolution overview

Resolution follows one of two paths depending on whether the mapping is the root partial method or a nested type mapping (collection elements, dictionary keys/values, tuple items, constructor parameters, or property initializers).

Root partial methods

Root partial methods use TypeMapIdentifierAlgorithm, which runs the detector chain described below. Root methods do not invoke an existing map method for themselves.

Nested type mappings

Nested mappings use TypeMapIdentifierWithMapMethodAlgorithm, which runs an existing-method pre-step first (see below). If no existing method applies, the same detector chain is used.

Existing-method pre-step (nested mappings only)

Before the detector chain, TypeMapIdentifierWithMapMethodAlgorithm checks whether a suitable map method already exists:

  • When:
    • A method in the same class or a base class of the mapper from TSource to TTarget exists OR,
    • A method from a property or field marked with the [MappaDependency] on the mapper or an accessible base class of the mapper, or from a base class of the dependency type from TSource to TTarget exists OR,
    • A method from a type defined via the [MappaStaticDependency] attribute exists OR,
    • A polymorphic method where one of the [MappaTypeMapping] attributes matches TSource and TTarget OR,
    • Setting PolymorphicMapMethodWithMatchingDefaultAttribute is Enable and a polymorphic method with attribute [MappaTypeMappingDefault] and behaviour MappaTypeMappingDefaultBehavior.MapSourceType, where TTarget matches the target type defined by the attribute and TSource matches the source type of the method, exists OR,
    • Setting CompatibleMapMethod is Enable and a method exists whose parameter type is a base type or interface of TSource and whose return type is a derived type of TTarget (selected after exact and nullability-relaxed matches fail);
  • What:
    • The existing method is invoked;
  • Notes:
    • A nested mapper method that requires MappaContext is only invoked when the caller provides context (the root map method has a MappaContext parameter);
    • This pre-step is used when mapping elements of an array, keys or values of dictionaries, elements of tuples, and properties or constructor parameters of class/struct/record types;
    • When #nullable enable is active, nullability annotations must match exactly first. If no exact match exists, the generator may reuse a method with a relaxed nullability signature on the same underlying types: nested mapping to TTarget? may invoke a method returning TTarget, and nested mapping from TSource may invoke a method accepting TSource?.

Detector chain order

When no existing method applies (or for root methods), TypeMapIdentifierAlgorithm tries the following strategies in order:

  1. Identity (skipped when the root method has one or more [MappaTypeMapping] or protobuf polymorphism attributes)
  2. Nullable
  3. Polymorphic (only at the root, or immediately after nullable unwrapping — not at every nested level) 3b. Protobuf oneof polymorphic (only at the root, or immediately after nullable unwrapping; when the root method has protobuf polymorphism attributes)
  4. Enum 4b. Bson well-known types (only when BsonWellKnownTypes is Enable) 4c. Protobuf well-known types (only when ProtobufWellKnownTypes is Enable) 4d. OneOf well-known types (only when OneOfWellKnownTypes is Enable)
  5. String
  6. Date & time 7a. IQueryable projection (root methods with IQueryable<TSource> → IQueryable<TTarget> only)
  7. Container
  8. Tuple
  9. Guid
  10. Constructor

Every GetStrategy discovery is wrapped with compile-time depth tracking (MaxCompileTimeDepth, default 50) and source/target type-pair cycle detection (MP00076 / MP00077). When BreakCompileTimeCycles is enabled, cycle edges synthesize private map methods and report MP00078 instead of MP00077. Details: Documentation/mappa-generator-algorithm.md — Reference handling and compile-time guards.

Reference handling

When ReferenceReusing is enabled and the map method declares MappaContext, generated code caches MappaReferenceManager in one local per map method (via an UnsafeAccessor getter), looks up pairs with TryGetReference<TTarget> (key: source identity + typeof(TTarget)), and registers with AddReferencePair<TTarget, TSource> early after construction so cycles can resolve. A redundant exit AddReferencePair is omitted when early registration already ran or a reusing nested map method already registered. MaxRuntimeDepth (short; effective default 0 = unlimited) limits nested reference-type mapping depth at runtime and throws MappaException when exceeded. Missing MappaContext warns with MP00074 / MP00075 and skips runtime reference handling. ReferenceReusing / MaxRuntimeDepth are rejected on IQueryable projection methods (MP00057).

Strategy details

1. Identity strategy

  • When:
    • TSource and TTarget are the same type (e.g. TSource => int and TTarget => int) OR,
    • TSource can be implicitly converted into TTarget (e.g. TSource => int and TTarget => long) OR,
    • TSource maps to object or object? (nullable-context rules apply: in #nullable enable, T maps to object?; in #nullable disable, T maps to object; when the source is not explicitly non-nullable annotated, T may map to non-nullable object);
  • What:
    • By default (ShallowCopy), the input value is assigned to the target;
    • When IdentityMapDeepCopy is DeepCopy on a reference-type same-type root, the mapper clones the instance via Mappa.MappaCloning.MemberwiseClone (nested references remain shared);
    • When IdentityMapDeepCopy is NestedDeepCopy on a reference-type root, the mapper clones the instance and recursively maps every accessible instance field; on a struct root, the struct is copied and fields are mapped recursively (no MemberwiseClone on the struct itself);
    • Primitives, enums, and string same-type mappings always assign and ignore IdentityMapDeepCopy;
  • Notes:
    • DateTime → DateTimeOffset is handled by the identity strategy (implicit conversion);
    • The identity detector is skipped when the root method has [MappaTypeMapping] attributes so that polymorphic mapping can run instead;
    • Same-type constructor-parameter identity in the constructor detector always uses shallow pass-through; IdentityMapDeepCopy applies only when the identity strategy is selected directly;
    • NestedDeepCopy on array or collection same-type roots falls through to the container strategy so elements are mapped;
    • Nested field resolution disables the identity detector to avoid infinite recursion.

2. Nullable strategy

  • When:
    • TSource is the Nullable<T> value type (e.g. int?) OR,
    • TSource is a nullable reference type when #nullable enable (e.g. string?) OR,
    • TSource is a reference type when #nullable disable (e.g. string — treated as a nullable source, consistent with IsReferenceNullable) OR,
    • TTarget is a nullable value type and TSource is the corresponding non-nullable value type,
    • AND a mapping exists from source to target when nullability is stripped (or, for nullable target / non-nullable source, a mapping exists for the inner types);
  • What:
    • When the source is nullable: if TTarget can be null, the mapper returns null; if TTarget cannot be null, the mapper throws a NullReferenceException;
    • When the target is nullable and the source is non-nullable: the inner type is mapped without a nullable wrapper;
  • Notes:
    • After nullable unwrapping, the polymorphic detector may run if the root method has [MappaTypeMapping] attributes.

3. Polymorphic mapping

  • When:
    • The root method has one or multiple [MappaTypeMapping] attributes, and the polymorphic detector is allowed to run (at the root, or immediately after the nullable detector);
  • What:
    • The runtime type of the input determines how the mapping from TSource to TTarget happens;
    • For every [MappaTypeMapping] attribute a mapping is created;
    • The default mapping when the actual parameter type does not match any source type from the [MappaTypeMapping] attributes is determined by the [MappaTypeMappingDefault] attribute — if the attribute is not present, the default behaviour throws an exception;
  • Notes:
    • The polymorphic detector does not run at every nested level; it runs only at the root or immediately after nullable unwrapping.

3b. Protobuf oneof polymorphic mapping

  • When:
    • The root method has any of [MappaProtobufPolymorphismSetup], [MappaProtobufTypeMapping] or [MappaProtobufTypeMappingDefault], and the detector is allowed to run (at the root, or immediately after the nullable detector). The classic polymorphic detector does not run for such a method;
  • What:
    • Validates the attributes: no [MappaTypeMapping] / [MappaTypeMappingDefault] on the same method, exactly one setup, the case property exists on the protobuf side(s) and is of a nested *OneofCase enum type, arm case names are valid (not None, no duplicates), arm properties are message-typed (scalar arms are rejected) and arm CLR types are compatible with the method types;
    • Resolves the scenario: proto→POCO (switch on the source {Oneof}Case), POCO→proto (switch on the runtime type of the source) or proto→proto (switch on the source case, assign the target arm);
    • For every [MappaProtobufTypeMapping] an arm strategy is resolved for the arm message/POCO types with the regular detector chain;
    • Proto targets are created with the empty constructor and the selected oneof property is assigned inside the arm; other fields of the target message are not mapped;
    • The default branch is determined by [MappaProtobufTypeMappingDefault]: Throw (default when absent), NoMap (leave the target oneof unset, protobuf targets only) or MapTo (map the unmatched source into the message of a named target arm, protobuf targets only);
  • Notes:
    • The source case None is handled by the default branch;
    • Failures are reported with diagnostics MP00093–MP00109, plus MP00006 when an arm (or the MapTo message) cannot be mapped and MP00021 when the Throw exception type is not an exception.

4. Enum strategy

  • When:
    • TSource is an enum and TTarget is a different enum, an integral numeric type compatible with the enum, or a string OR,
    • TSource is an integral numeric type compatible with the enum and TTarget is an enum OR,
    • TSource is a string and TTarget is an enum;
  • What:
    • Enum → enum and enum → string: a switch statement maps by shared enum member names, by shared underlying numeric values when EnumToEnumMapSetting is NumericValue, or by shared [Description] attribute values when EnumStringMapSetting or EnumToEnumMapSetting is Description;
    • When mapping enum → enum, if at least one source enum member cannot be paired with a target member (by name, numeric value, or Description, depending on EnumToEnumMapSetting), the generator reports warning MP00039 and continues code generation; unmapped source values throw ArgumentOutOfRangeException at runtime;
    • When Description mapping is enabled, every enum member involved must have a non-empty [Description] attribute; otherwise the generator reports error MP00040 and does not generate the mapping;
    • When Description mapping or case-insensitive member-name mapping would pair multiple source members with the same target member, the generator reports error MP00041 and does not generate the mapping;
    • Enum → integral: maps enum members to their underlying integral values;
    • Integral → enum: maps integral values (cast to the enum underlying type) to enum members;
    • String → enum: maps string input to enum members by name or by [Description] value (handled by the enum strategy, not the string strategy, because the enum detector runs first); when CaseInsensitiveEnumMap is enabled in MappaSettings, member names or Description values are matched case-insensitively via ToUpperInvariant();
    • [MappaMapEnumMember], [MappaMapEnumIgnore], and [MappaMapEnumDefault] on the map method are merged with settings-based pairing by EnumMapConfigurationResolver; explicit member overrides take precedence, ignored members are omitted, and unmapped values use the configured default arm (Throw or UseDefaultValue);
  • Notes:
    • The enum detector runs before the general string strategy, so string → enum is handled here rather than by the string strategy;
    • Enum → enum matching uses member names by default; set EnumToEnumMapSetting to NumericValue to match by underlying numeric value, or to Description to match by [Description] attribute value;
    • Enum ↔ string matching uses member names by default; set EnumStringMapSetting to Description to match by [Description] attribute value;
    • String → enum and enum → enum member-name matching is case-sensitive by default; enable CaseInsensitiveEnumMap for case-insensitive member name or Description matching;
    • Warning MP00039 is suppressed for members excluded via [MappaMapEnumIgnore] and when [MappaMapEnumDefault] is configured with UseDefaultValue;
    • Errors MP00047–MP00054 halt mapping generation when enum configuration attribute validation fails (see enum mapping configuration attributes).

4b. Bson well-known types strategy

  • When: BsonWellKnownTypes is Enable and MongoDB.Bson types are available — string/byte[] ↔ ObjectId, or Decimal128 → decimal.
  • What: Emits ObjectId.Parse / new ObjectId / ToString / ToByteArray / Decimal128.ToDecimal as appropriate.
  • Notes: Opt-in; consumer references MongoDB.Bson. Runs before the string strategy. See algorithm §4b.

4c. Protobuf well-known types strategy

  • When: ProtobufWellKnownTypes is Enable and well-known protobuf types are available — Timestamp/Duration and scalar wrappers.
  • What: Emits library helpers (FromDateTime, ToTimeSpan, wrapper .Value, etc.).
  • Notes: Opt-in; consumer references Google.Protobuf. Distinct from ProtobufOptional. See algorithm §4c.

4d. OneOf well-known types strategy

  • When: OneOfWellKnownTypes is Enable and OneOf / OneOfBase types are available — Option unwrap/wrap, unique-arm general unwrap, and constructible OneOfBase wrap.
  • What: Emits IsTn / AsTn / FromTn checks and MappaException on missing arms / None (or null/default for Option under NRT rules).
  • Notes: Opt-in; consumer references OneOf. Exact-arm T → OneOf<…, T, …> via identity remains ungated. IQueryable projection involving OneOf is unsupported. See algorithm §4d.

5. String strategy

  • When:
    • TSource is a string and TTarget is any of the numeric types OR,
    • TSource is a string and TTarget is any of the following types: DateTime, DateTimeOffset, DateOnly, TimeOnly, TimeSpan, Guid, Uri OR,
    • TTarget is a string OR,
    • TSource is a string and TTarget is any type with an accessible static Parse(string) method;
  • What:
    • TSource is a string and TTarget is any of the numeric types: the relevant Parse method is invoked, possibly with the culture and NumberStyles from MappaSettings;
    • When parsing numeric types from string, if a style is configured the generator emits the BCL overload that accepts NumberStyles: Parse(input, style) when culture is not configured, or Parse(input, style, culture) when culture is configured. The resolved style is the type-specific NumberStyles when set, otherwise GlobalNumberStyle. When neither is configured, the overload without NumberStyles is used;
    • TSource is a string and TTarget is DateTime, DateTimeOffset, DateOnly, TimeOnly, TimeSpan, or Guid: the relevant Parse or ParseExact method is used, possibly with format, culture, and DateTimeStyles from MappaSettings;
    • When parsing DateTime, DateTimeOffset, DateOnly, or TimeOnly from string, if a style is configured the generator emits the BCL overload that accepts DateTimeStyles as the last parameter: Parse(input, culture, style) or ParseExact(input, format, culture, style). The resolved style is the type-specific DateTimeStyles when set, otherwise GlobalDateTimeStyle. When culture is not configured, null is passed for IFormatProvider. When neither type-specific nor global style is configured, the overload without DateTimeStyles is used;
    • When a DateTimeStyles or NumberStyles value from [MappaSettings] contains undefined bits (for example an arbitrary integer cast such as (DateTimeStyles)999), the generator reports warning MP00038 and continues code generation using only the recognized flag bits in the emitted parse call. Values from .editorconfig that cannot be parsed are ignored without a diagnostic;
    • TSource is a string and TTarget is Uri: System.UriBuilder is used;
    • TTarget is a string and TSource is DateTime, DateTimeOffset, DateOnly, TimeOnly, TimeSpan, or Guid: the relevant ToString() overload is used, possibly with format and culture from MappaSettings;
    • TTarget is a string and TSource is any numeric type: the relevant ToString overload is used, possibly with format and culture from MappaSettings;
    • TTarget is a string: TSource.ToString() is used;
    • TSource is a string and TTarget has an accessible static Parse(string) method: the Parse method is invoked;
  • Notes:
    • string → enum is handled by the enum strategy, not the string strategy;
    • When an accessible static Parse(string) exists, the string/Parse strategy runs before the constructor strategy; a single-string-parameter constructor does not take priority over static Parse.

6. Date & time strategy

  • When:
    • TSource is a DateTime and TTarget is long, DateOnly, or TimeOnly OR,
    • TSource is a DateTimeOffset and TTarget is long, DateOnly, TimeOnly, or DateTime OR,
    • TSource is a DateOnly and TTarget is long or DateTime OR,
    • TSource is a long or any smaller numeric type implicitly castable to long, and TTarget is DateTime or DateTimeOffset OR,
    • TSource is a TimeSpan and TTarget is double OR,
    • TSource is a double and TTarget is TimeSpan;
  • What:
    • The usual mapping conversions are used;
    • When mapping from DateOnly to DateTime, TimeOnly.MinValue is combined with DateTimeKind.Utc;
    • When mapping to or from long, Unix time is used;
    • When a timezone is required, UTC is implied;
  • Notes:
    • DateTime → DateTime is handled by the identity strategy, not the date & time strategy;
    • DateTime → DateTimeOffset is handled by the identity strategy;
    • DateTimeOffset → DateTime is handled by the date & time strategy (not identity);
    • There is no dedicated DateOnly → DateTimeOffset strategy in the current implementation (only DateOnly → DateTime).

7a. IQueryable projection strategy

  • When:
    • The root map method source type implements or is IQueryable<TSource>,
    • The root map method target type implements or is IQueryable<TTarget>,
    • TSource and TTarget are different types,
    • An element mapping from TSource to TTarget can be expressed as a projection-compatible expression (constructor/property mapping, identity, nullable, enums, and common primitive/date conversions),
    • The root method does not declare MappaBeforeMap / MappaAfterMap hooks or a MappaContext parameter;
  • What:
    • The generator emits return source.Select(element => /* projection expression */);,
    • Element mapping is validated by ProjectionCapabilityAnalyzer and built by ProjectionExpressionBuilder (expression-tree-compatible code, not imperative statements),
  • Notes:
    • Projection is signature-driven by default; optional [MappaProjection] on the class or method opts in (Enabled = Enable) or out (Enabled = Disable falls through to the container / eager path),
    • Mapping attributes such as [MappaUseProperty] on the projection method participate in element construction,
    • Nested collections (List<T> / arrays) are projected via nested Enumerable.Select plus ToList / ToArray; nested IQueryable properties remain unsupported (MP00059),
    • Value tuples and common Guid↔string / Guid↔byte[] conversions are supported via expression builders (value tuples emit new System.ValueTuple<...>(...) because tuple literals are illegal in expression trees),
    • Projection* settings override non-prefixed enum/format/culture settings for the projection element context only,
    • When a companion map method is replaced by a re-derived structural strategy, warning MP00086 is reported,
    • Nested IQueryable properties, polymorphic root element maps, non-inlinable invoke methods, and before/after hooks are rejected with dedicated diagnostics (MP00055–MP00060),
    • [MappaObjectFactory] on a projection map for the element target is rejected with MP00064,
    • [MappaAllowInaccessibleSourceMembers] / [MappaAllowInaccessibleTargetMembers] on a projection map are rejected with MP00069,
    • ReferenceReusing / MaxRuntimeDepth on a projection map are rejected with MP00057,
    • Mapping IQueryable<TSource> to a concrete collection (for example List<TTarget>) is not a projection: the container path applies and may emit warning MP00061,
    • Prefer numeric or description enum mappings over case-insensitive member-name matching for ORM providers (warning MP00060),
    • Generated projection methods are annotated with [RequiresDynamicCode] and are not compatible with Native AOT deployment.

7. Container strategy

  • When:
    • TSource and TTarget are both either dictionaries or collections,
    • For dictionaries, mappings exist from source key type to target key type and from source value type to target value type,
    • For collections, a mapping exists from the source element type to the target element type,
    • Neither side is an IQueryable<T> participating in a projection pair (queryable sources/targets are excluded from eager collection materialization when projection applies; see §7a),
    • TSource dictionary types accepted:
      • any type implementing IDictionary<TKey, TValue>;
      • any type implementing IReadOnlyDictionary<TKey, TValue>;
      • any type implementing IEnumerable<KeyValuePair<TKey, TValue>>;
    • TTarget dictionary types accepted:
      • any type implementing IDictionary<TKey, TValue> that has a constructor with zero arguments;
      • the following interfaces: IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>, IImmutableDictionary<TKey, TValue>;
      • the following classes: ImmutableDictionary<TKey, TValue>, ImmutableSortedDictionary<TKey, TValue>, FrozenDictionary<TKey, TValue>, ConcurrentDictionary<TKey, TValue>;
    • TSource collection types accepted: any type implementing IEnumerable<T>, arrays, Span<T>, ReadOnlySpan<T>, Memory<T>, and ReadOnlyMemory<T>;
    • TTarget collection types accepted:
      • any type implementing ICollection<T> or ISet<T> that has a constructor with zero arguments (or one constructor with one integer argument of type int when MappaSettings.ContainerCapacityConstructors is enabled);
      • any type derived from Stack<T>, Queue<T>, or BlockingCollection<T> that has a constructor with zero arguments (or one constructor with one integer argument when MappaSettings.ContainerCapacityConstructors is enabled);
      • the following interfaces: IEnumerable<T>, ICollection<T>, IReadOnlyCollection<T>, ISet<T>, IList<T>, IReadOnlyList<T>, IReadOnlySet<T>, IImmutableSet<T>, IImmutableList<T>, IImmutableQueue<T>, IImmutableStack<T>, IProducerConsumerCollection<T>;
      • the following classes: arrays, List<T>, ReadOnlyCollection<T>, Span<T>, ReadOnlySpan<T>, Memory<T>, ReadOnlyMemory<T>, Stack<T>, Queue<T>, ReadOnlySet<T>, HashSet<T>, SortedSet<T>, FrozenSet<T>, ImmutableHashSet<T>, ImmutableSortedSet<T>, ImmutableArray<T>, ImmutableList<T>, ImmutableQueue<T>, ImmutableStack<T>, ConcurrentBag<T>, ConcurrentQueue<T>, ConcurrentStack<T>;
  • What:
    • A for loop or foreach loop is added to the code;
    • In the loop, each element from the source collection is mapped to an element of the target collection and then added to the target collection;
  • Notes:
    • When possible for some types (e.g. List<T>), the constructor accepting capacity is preferred to reduce allocations (ContainerCapacityConstructors);
    • When MappaSettings.FastCollections (or the corresponding .editorconfig setting) is enabled, collection mapping may use span-based iteration for array and List<T> sources and may allocate target arrays with a known capacity;
    • When MappaSettings.ReplaceNullWithEmptyContainer (or mappa.replacenullwithemptycontainer) is enabled, a null source collection or dictionary maps to an empty target instead of null / throw;
    • When MappaSettings.PreventEnumerableCount (or the corresponding .editorconfig setting) is enabled, mapping from a source without Count or Length to a fixed-size target such as T[], Span<T>, ReadOnlySpan<T>, Memory<T>, or ReadOnlyMemory<T> uses a growable List<T> buffer instead of calling Enumerable.Count, so the source is enumerated only once;
    • When MappaSettings.EnumerableConcreteType is Array (or the corresponding .editorconfig setting), sequence-like interface targets such as IEnumerable<T>, IList<T>, ICollection<T>, IReadOnlyList<T>, and IReadOnlyCollection<T> use a T[] buffer instead of List<T>; concrete List<T> targets are unchanged;
    • When MappaSettings.DictionaryAssignment is Add (or the corresponding .editorconfig setting), dictionary-to-dictionary mappings insert entries with IDictionary<TKey, TValue>.Add(key, value) instead of the indexer; explicit IDictionary<K, V> implementations use a typed interface temporary when required; the default is indexer assignment;
    • Explicit interface implementation is supported;
    • Types with an empty constructor are supported if they are derived from any of the supported interfaces or classes;
    • When a type implements multiple closed versions of the same collection/dictionary interface family, preferred-interface resolution (attribute → unique usable implicit → unique usable explicit) selects element/key/value types and codegen access; see Documentation/mappa-attributes.md — Preferred interface implementations and diagnostics MP00079–MP00085.

8. Tuple strategy

  • When:
    • TSource and TTarget are both tuple types,
    • The number of elements in the tuple is the same,
    • For each element of the tuple there exists a mapping from source element to target element;
  • What:
    • Each element of the source tuple is mapped into a new element of the target tuple;
    • The target tuple is created by combining the mapped elements;
  • Notes:
    • Both named and unnamed tuples are supported;
    • The type Tuple<T> (and its variations with more elements) is supported.

9. Guid strategy

  • When:
    • TSource is Guid and TTarget is byte[], Span<byte>, ReadOnlySpan<byte>, Memory<byte>, or ReadOnlyMemory<byte>, OR
    • TSource is byte[], Span<byte>, ReadOnlySpan<byte>, Memory<byte>, or ReadOnlyMemory<byte> and TTarget is Guid;
  • What:
    • A mapping from Guid to byte[]/Span<byte> is defined using the relevant Guid constructors or the Guid.ToByteArray() method.

10. Constructor strategy

The constructor strategy tries sub-strategies in order. A valid [MappaUseConstructor] on the root map method forces empty or exact parameterized construction for that target type (after object factories):

Sub-strategy When
Object factory A [MappaObjectFactory] registration on the root map method (class ∪ method attributes) resolves to an accessible factory for the exact target type
Forced use-constructor A valid [MappaUseConstructor] targets this type (NoParameters or exact WithParameters signature)
Mapping constructor An accessible constructor TTarget(TSource input) exists where the single parameter type matches the source type (or an inaccessible matching constructor when [MappaAllowInaccessibleTargetMembers] allows constructors)
Empty constructor + property init An accessible zero-argument constructor exists; settable properties (and, when MapTargetFields is enabled, writable instance fields) and get-only collections per the notes below can be mapped from the source (or an inaccessible empty constructor when allowed). Empty targets (no mappable candidates and no eligible instance fields) still emit new TTarget() at root and nested sites; candidates with none mapped still fail empty-ctor selection
Parameterized constructor No suitable empty-constructor path; the accessible constructor with the most mappable parameters is chosen (parameter names are matched to source properties — and source fields when MapSourceFields is enabled — case-insensitively by default) (inaccessible parameterized constructors participate when allowed)
  • What:
    • Object factory: invoke the resolved factory; depending on signature tier, either return the factory result as-is, fill properties like the empty-constructor path after the call, or map factory parameters like the parameterized-constructor path;
    • Forced use-constructor: empty-constructor or exact parameterized/mapping-constructor path; failures report MP00087–MP00090 without falling through; unused attributes warn MP00092;
    • Otherwise each property, constructor argument, or mapping-constructor parameter is mapped and a new instance of TTarget is generated with new (or via a generated UnsafeAccessor constructor helper when the selected constructor is inaccessible);
    • Get-only dictionary or collection properties for which a mapper exists are filled with mapped values from the corresponding source;
  • Notes:
    • Object factories are keyed by exact target type and apply to nested mappings whose target type matches a registration on the root map method;
    • Duplicate factories for the same target type report error MP00062; unresolved factories warn with MP00063 and fall through to constructor strategies; factories on IQueryable projection maps report error MP00064;
    • Factory location and signature tiers are documented in Mappa attributes — MappaObjectFactory;
    • [MappaUseConstructor] is method-only; see Mappa attributes — MappaUseConstructor and diagnostics MP00087–MP00092;
    • Empty-constructor path: member name matching is case-sensitive by default, configurable via CaseInsensitivePropertyMap and IgnoreUnderscoreForPropertyMap in MappaSettings; enable MapSourceFields / MapTargetFields to include accessible instance fields (property preferred when both match the same name); empty targets (no properties after ignore filters and no eligible instance fields) are allowed at root and nested sites via new TTarget() with no initializers; types that only have instance fields are not treated as empty when MapTargetFields is off;
    • Parameterized-constructor path: constructor parameter names are matched to source properties (and source fields when MapSourceFields is enabled) case-insensitively by default, with optional underscore-insensitive matching via IgnoreUnderscoreForPropertyMap; when MapTargetExplicitInterfaceProperties is enabled, remaining explicitly implemented interface target properties are assigned after construction via an interface cast;
    • Explicit interface implementation is supported for get-only dictionary and collection properties; scalar explicitly implemented interface properties are mapped when MapSourceExplicitInterfaceProperties / MapTargetExplicitInterfaceProperties is enabled (via cast to the declaring interface; never via UnsafeAccessor);
    • When both Map* explicit-interface settings and [MappaAllowInaccessible*] are enabled, ordinary private members use UnsafeAccessor while explicit interface properties use interface casts;
    • Get-only Stack<T>, Queue<T>, ConcurrentStack<T>, ConcurrentQueue<T>, ConcurrentBag<T>, and BlockingCollection<T> properties are filled post-construction using Push, Enqueue, or Add respectively;
    • The following attributes on the map method can override mapping behaviour (see Documentation/mappa-attributes.md for details):
      • [MappaUseProperty]
      • [MappaIgnoreTargetProperty] (empty-constructor / empty-ctor-like factory path only)
      • [MappaMustMapTargetProperty] (empty-constructor / empty-ctor-like factory path only; flat property/field names; unmapped must-map members report MP00065)
      • [MappaAllowInaccessibleSourceMembers] (opt-in private/protected source property (and field when MapSourceFields) reads via UnsafeAccessor; flat names or all; MP00067 / MP00069)
      • [MappaAllowInaccessibleTargetMembers] (opt-in private/protected target property/field writes and/or inaccessible constructors via UnsafeAccessor; AllowProperties / AllowFields / AllowConstructors; MP00067–MP00069)
      • [MappaAssignFromContext]
      • [MappaAssignFromConstant]
      • [MappaNullSubstitute] — substitutes a null source member with AssignValue or InvokeMethod before mapping continues; optional SourcePropertyName; enables T? → non-nullable T for that member; MP00115–MP00119
      • [MappaInvokeMethod] — optionally accepts SourcePropertyName to select the source property (or field) passed to the invoked method
      • [MappaAssignToContext] (post-construction context writes; requires the caller to provide MappaContext)
    • Those attributes accept flat names or dot-separated nested property paths (see also algorithm — nested property paths) (except the inaccessible-member attributes, which accept flat names only); path segments may name fields when MapSourceFields / MapTargetFields are enabled;
    • When inaccessible setters, inaccessible field writes, or unsafe-accessor constructors are used, assignments that would normally use an object initializer are emitted after construction; helpers are generated in a file-local __MappaInaccessibleAccessors type in the mapper's namespace;
    • When MappaSettings.ProtobufOptional is enabled, optional protobuf members are handled via companion Has* properties on the source and target types.
    • Accessible fields in IQueryable projections are supported when field settings are enabled; inaccessible fields remain projection-forbidden (MP00069).

Nested property paths

Leaf source chains use conditional access based on each receiver type, reuse nested source temps, and append ?? throw new System.NullReferenceException("...") only when the expression can be null and the target cannot (never on plain non-nullable value-type leaves). Nested [MappaIgnoreTargetProperty] applies alongside sibling nested attributes under the same root. Nested [MappaAssignToContext] reads the leaf from the constructed result after mapping. Path segments may name fields when field mapping settings are enabled. Details: Documentation/mappa-generator-algorithm.md — Nested property paths.

Before and after map hooks

Root mapping methods may declare [MappaBeforeMap] and [MappaAfterMap] on the mapper class and/or method. Hooks apply only to generated root methods. Resolution prefers ref T + context, then ref T, then context, then parameterless (T is source for before hooks and target for after hooks). Before hooks run class-then-method; after hooks run method-then-class. Unresolved hooks warn with MP00045; class/method duplicates warn with MP00046 and invoke once. After hooks force materialization of the core mapping result into a local before the after-hook calls and return. Details: Documentation/mappa-generator-algorithm.md — Before and after map hooks.

Value transformers

Methods marked with [MappaTransformer] (and methods selected by [MappaApplyTransformer]) rewrite a value of type T into another T. After a strategy is selected for a mapping site it is wrapped in a ValueTransformerMapStrategy, so the source value is transformed before the inner strategy runs and the mapped value is transformed before it is assigned or returned, at the root and at every nested site (properties, constructor parameters, collection elements, dictionary keys/values, tuple items). Order per side: [MappaApplyTransformer] attributes, local [MappaTransformer] methods, base-class transformers, then dependency transformers when MappaTransformerFromDependency is Enable (class, method or mappa.transformerfromdependency). Transformers must match the site type exactly (nullability included) and may be T Method(T), T Method(in T) or either with a trailing MappaContext (only when the map method has context). Diagnostics: MP00110 (invalid signature), MP00111 (unresolved apply-transformer method), MP00112 (missing source member), MP00113 (ClassType with FieldName), MP00114 ([MappaApplyTransformer] on a projection). Transformers are never applied inside IQueryable projections; class-level [MappaTransformer] alone does not invalidate a projection.

See the algorithm for the full description.

Limitations

Currently unsupported features include:

  • Generic type parameters on map methods (for example, TTarget Map<TSource, TTarget>(TSource input)).
  • Nested IQueryable properties inside member initializers (nested collections are supported; see §7a).
  • Dictionary projections inside queryable element maps.
  • Provider-specific expression shapes that compile but are not translated by a given LINQ provider at runtime.
  • ReferenceReusing / MaxRuntimeDepth (and MappaContext) on IQueryable projection map methods.

Other relevant packages:

  • Mappa: attributes and MappaContext used to drive the source generator.

For Bson/Protobuf/OneOf well-known types, enable BsonWellKnownTypes / ProtobufWellKnownTypes / OneOfWellKnownTypes and reference MongoDB.Bson, Google.Protobuf, or OneOf.

You can find samples here. Visit the Mappa documentation to learn more.

There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

  • .NETStandard 2.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
10.3.0 113 10/25/2026
10.2.0 137 8/9/2026
10.1.0 146 7/4/2026
10.0.0 193 6/11/2026
1.2.0 304 5/11/2025
1.1.0 262 4/21/2025
1.0.0 239 3/29/2025