Document status: Canonical implementation specification for Mammoth.LiteMapper 1.0
Specification version: 1.0-draft.2
Product name: Mammoth.LiteMapper
License: MIT
Audience: implementation agents, maintainers, reviewers, package authors, and contributors
This file is the sole product specification until implementation stabilizes. A usage guide MUST NOT be treated as authoritative during initial development. Usage documentation MUST later be assembled from compiling sample projects.
The terms MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT, and MAY are normative.
- MUST / MUST NOT define mandatory behavior for LiteMapper 1.0.
- SHOULD / SHOULD NOT define strong recommendations that require a documented reason to violate.
- MAY defines an implementation choice that does not alter the public contract.
When this specification is ambiguous, contradictory, or incomplete, implementation MUST stop. The implementation agent MUST record the problem in DECISIONS.md and obtain an explicit decision before continuing any project work. It MUST NOT guess, silently select a behavior, or implement multiple competing semantics.
LiteMapper is a convention-first, compile-time object mapping library for C#.
A consumer declares partial mapping methods. A Roslyn incremental source generator emits direct C# implementations using constructors, assignments, loops, casts, and ordinary method calls.
LiteMapper 1.0 is intentionally focused. It is not a runtime mapping container.
The canonical product, solution, package, assembly, project, and root namespace prefix is Mammoth.LiteMapper.
The shorter name LiteMapper MAY be used in explanatory prose. It MUST NOT be used as the package ID, assembly name, project-file name, solution-file name, or root namespace.
The primary public mapper marker remains [LiteMapper], implemented by Mammoth.LiteMapper.LiteMapperAttribute. Public attribute and type names defined in section 5 are not otherwise prefixed with Mammoth.
The diagnostic prefix remains LITEMAPPER.
LiteMapper 1.0 MUST provide:
- compile-time generated object mapping;
- static and instance mapper classes;
- extension-method mapping for static mapper classes;
- flat object mapping;
- nested object mapping;
- arrays, common sequence collections, sets, and dictionaries;
- records, constructors,
initmembers,requiredmembers, structs, and record structs; - explicit member renaming and ignoring;
- compile-time validated source paths;
- custom converters written as ordinary C# methods;
- mapping into existing objects;
- configurable null semantics;
- enum mapping;
- recursive type mapping with optional runtime cycle detection;
- deterministic generated source;
- strong compile-time diagnostics;
- zero runtime reflection in all generated mapping paths;
- trimming and Native AOT compatibility for all supported generated features;
netstandard2.0consumer-library compatibility and first-class modern .NET consumer support.
LiteMapper 1.0 MUST NOT provide:
- a universal runtime
IMapperorMap<TDestination>(object)API; - runtime mapper registration or assembly scanning;
- runtime reflection fallback;
- dynamic dispatch based on runtime destination types;
- asynchronous mapping or asynchronous converters;
- EF Core or LINQ provider projections;
- runtime polymorphic derived-type mapping;
- object factories;
- before-map or after-map hooks;
- a mapping-context parameter;
- dependency-injection integration or assembly-scanned registration;
- reference identity preservation;
- open generic mapping methods;
- generic mapper classes;
- mapping inside generic containing types;
- automatic PascalCase flattening;
- underscore or member-prefix normalization;
- dotted target paths;
- automatic source-method discovery;
- immutable-collection support;
- custom-collection construction conventions;
- queues or stacks;
- rectangular multidimensional arrays;
Span<T>,ReadOnlySpan<T>, or automaticref structmapping;- pointer or function-pointer structural mapping;
dynamicmapping;- a code-fix provider;
- runtime logging hooks;
- telemetry.
Unsupported features MUST produce a documented diagnostic when they are presented as generated partial mappings. Handwritten methods remain ordinary C# and MAY implement behavior outside LiteMapper's generated feature set.
Every generated mapping MUST be completely resolvable from the consumer compilation. Generated code MUST NOT inspect types through reflection at runtime.
LiteMapper MUST generate implementations only for user-declared eligible partial methods. It MUST NOT generate unexpected public collection overloads, universal dispatch APIs, or public nested helper methods.
Custom behavior MUST be expressed with ordinary methods whenever possible. Mapper classes MAY contain both generated partial methods and handwritten methods.
Identical generator version, compilation inputs, parse options, analyzer options, and references MUST produce byte-for-byte identical generated source.
Generated output MUST NOT contain timestamps, random identifiers, absolute machine paths, user names, temporary directories, or non-deterministic member ordering.
The generator MUST detect capabilities from the consumer compilation rather than relying only on target-framework strings.
- C# language capabilities MUST be derived from
CSharpParseOptions.LanguageVersion. - Preprocessor symbols MAY be read from
CSharpParseOptions.PreprocessorSymbolNames. - Framework APIs MUST be detected using Roslyn symbols and exact member signatures.
- Target-framework metadata MAY guide optimization but MUST NOT be the sole proof that an API exists.
Target- or language-specific optimization MUST NOT change observable mapping semantics.
When LiteMapper cannot generate a correct mapping, it MUST report a diagnostic. It MUST NOT silently use reflection, dynamic invocation, default!, data filtering, partial collection updates, or throwing production stubs merely to make compilation succeed.
LiteMapper MUST produce these packages:
The primary supported installation package.
It MUST:
- reference
Mammoth.LiteMapper.Abstractionsas a normal dependency; - include the generator assembly as an analyzer asset under
analyzers/dotnet/cs, or provide an equivalent package layout that is proven by package-consumer tests; - require only one consumer
PackageReferencefor normal use; - prevent generator and Roslyn assemblies from becoming runtime dependencies or being copied to application output.
The optional advanced package containing public attributes, configuration enums, and LiteMapperCycleException.
It MUST target netstandard2.0.
It MUST NOT reference Roslyn packages or expose Roslyn types.
The generator implementation package.
It MUST:
- target
netstandard2.0unless a later measured optimization justifies additional analyzer assets; - contain the analyzer assembly under
analyzers/dotnet/cs; - keep
Microsoft.CodeAnalysisdependencies private; - not be advertised as the normal installation route.
The minimum supported compiler host is Roslyn 4.8.0. The generator MUST compile against the Microsoft.CodeAnalysis 4.8.0 API baseline unless an approved specification change raises that minimum. APIs introduced after the baseline MUST NOT be called directly by the baseline generator asset.
All shipping assemblies MUST target netstandard2.0 unless a benchmark and compatibility review explicitly approves an additional target-specific asset.
The implementation MUST NOT multi-target assemblies merely to use newer syntax or produce cosmetically modern binaries.
LiteMapper 1.0 MUST test and support consumers targeting:
netstandard2.0;net8.0;net9.0;net10.0.
A netstandard2.0 consumer MUST use an SDK/compiler capable of C# 9 or newer.
Older .NET and .NET Framework consumers are not part of the supported test matrix, even when package compatibility happens to permit installation.
The minimum supported language version is C# 9.
The generator MUST detect the effective configured language version and emit compatible syntax. It SHOULD emit conservative C# 9-compatible code unless newer syntax provides a measured or necessary capability benefit.
The generator MUST NOT emit syntax unavailable in the configured consumer language version.
Conditional compilation MAY be used:
- internally when building intentionally different generator assets;
- in generated source when it is the safest way to support a multi-targeted consumer;
- to select optimized APIs guarded by symbols and verified symbol availability.
Direct capability-selected generation is preferred because a source generator runs separately for each target-framework compilation.
Conditional compilation MUST NOT create different mapping semantics across targets.
The repository MUST use this high-level layout:
src/
Mammoth.LiteMapper/
Mammoth.LiteMapper.csproj
Mammoth.LiteMapper.Abstractions/
Mammoth.LiteMapper.Abstractions.csproj
Mammoth.LiteMapper.Generator/
Mammoth.LiteMapper.Generator.csproj
tests/
Mammoth.LiteMapper.Generator.Tests/
Mammoth.LiteMapper.Generator.Tests.csproj
Mammoth.LiteMapper.Runtime.Tests/
Mammoth.LiteMapper.Runtime.Tests.csproj
Mammoth.LiteMapper.Packaging.Tests/
Mammoth.LiteMapper.Packaging.Tests.csproj
Mammoth.LiteMapper.IntegrationTests/
Mammoth.LiteMapper.IntegrationTests.csproj
samples/
Mammoth.LiteMapper.Samples.Basic/
Mammoth.LiteMapper.Samples.Basic.csproj
Mammoth.LiteMapper.Samples.Collections/
Mammoth.LiteMapper.Samples.Collections.csproj
Mammoth.LiteMapper.Samples.AspNetCore/
Mammoth.LiteMapper.Samples.AspNetCore.csproj
benchmarks/
Mammoth.LiteMapper.Benchmarks/
Mammoth.LiteMapper.Benchmarks.csproj
The repository MUST contain both solution formats with these exact file names:
Mammoth.LiteMapper.sln
Mammoth.LiteMapper.slnx
Every project file, assembly name, root namespace, and default package ID defined by this specification MUST use the Mammoth.LiteMapper prefix.
Implementation MUST create and maintain:
AGENTS.md
IMPLEMENTATION_PLAN.md
DECISIONS.md
STATUS.md
AGENTS.md MUST state at least:
- this specification is authoritative;
- runtime reflection is forbidden;
- undocumented public APIs are forbidden;
- milestones are test-first;
- focused and full tests must run;
- diagnostics must not be suppressed merely to pass tests;
- semantic decisions must not be changed silently;
- generated output must remain deterministic;
- status and decisions must be updated after each milestone.
All public abstraction types MUST live directly in namespace Mammoth.LiteMapper.
The library MUST NOT provide a [Mapper] alias. The mapper attribute is [LiteMapper].
All attribute classes MUST be sealed and use ordinary set accessors for named properties. Consumers MUST NOT derive custom attributes for generator interpretation.
Every configurable enum MUST reserve Unspecified = 0 for configuration inheritance.
namespace Mammoth.LiteMapper;
public enum NameMatching
{
Unspecified = 0,
Exact = 1,
ExactThenIgnoreCase = 2,
IgnoreCase = 3
}
public enum UnmappedMemberPolicy
{
Unspecified = 0,
Ignore = 1,
Info = 2,
Warning = 3,
Error = 4
}
public enum NullableMismatchPolicy
{
Unspecified = 0,
Error = 1,
Throw = 2
}
public enum NullCollectionStrategy
{
Unspecified = 0,
Error = 1,
Preserve = 2,
Empty = 3
}
public enum NumericConversion
{
Unspecified = 0,
ImplicitOnly = 1,
Checked = 2,
Unchecked = 3
}
public enum EnumMappingStrategy
{
Unspecified = 0,
ByName = 1,
ByValue = 2
}
public enum EnumNumericConversion
{
Unspecified = 0,
Checked = 1,
Unchecked = 2
}
public enum UnmatchedEnumValuePolicy
{
Unspecified = 0,
Error = 1,
Throw = 2,
ByValue = 3
}
public enum ReferenceHandling
{
Unspecified = 0,
None = 1,
ThrowOnCycle = 2
}
public enum OptionState
{
Unspecified = 0,
Disabled = 1,
Enabled = 2
}namespace Mammoth.LiteMapper;
[AttributeUsage(
AttributeTargets.Class,
AllowMultiple = false,
Inherited = false)]
public sealed class LiteMapperAttribute : Attribute
{
public NameMatching NameMatching { get; set; }
public UnmappedMemberPolicy UnmappedTargetMembers { get; set; }
public UnmappedMemberPolicy UnmappedSourceMembers { get; set; }
public NullableMismatchPolicy NullableMismatch { get; set; }
public NullCollectionStrategy NullCollections { get; set; }
public NumericConversion NumericConversion { get; set; }
public bool AllowExplicitOperators { get; set; }
public EnumMappingStrategy EnumMapping { get; set; }
public EnumNumericConversion EnumNumericConversion { get; set; }
public UnmatchedEnumValuePolicy UnmatchedEnumValues { get; set; }
public ReferenceHandling ReferenceHandling { get; set; }
public bool GuardNonNullSource { get; set; }
public bool IgnoreNullSourceMembers { get; set; }
}Boolean properties on LiteMapperAttribute default to false. They do not participate in assembly-level inheritance because assembly defaults do not expose those options.
namespace Mammoth.LiteMapper;
[AttributeUsage(
AttributeTargets.Method,
AllowMultiple = false,
Inherited = false)]
public sealed class MappingOptionsAttribute : Attribute
{
public NameMatching NameMatching { get; set; }
public UnmappedMemberPolicy UnmappedTargetMembers { get; set; }
public UnmappedMemberPolicy UnmappedSourceMembers { get; set; }
public NullableMismatchPolicy NullableMismatch { get; set; }
public NullCollectionStrategy NullCollections { get; set; }
public NumericConversion NumericConversion { get; set; }
public OptionState AllowExplicitOperators { get; set; }
public EnumMappingStrategy EnumMapping { get; set; }
public EnumNumericConversion EnumNumericConversion { get; set; }
public UnmatchedEnumValuePolicy UnmatchedEnumValues { get; set; }
public ReferenceHandling ReferenceHandling { get; set; }
public OptionState GuardNonNullSource { get; set; }
public OptionState IgnoreNullSourceMembers { get; set; }
}namespace Mammoth.LiteMapper;
[AttributeUsage(
AttributeTargets.Assembly,
AllowMultiple = false,
Inherited = false)]
public sealed class LiteMapperDefaultsAttribute : Attribute
{
public NameMatching NameMatching { get; set; }
public UnmappedMemberPolicy UnmappedTargetMembers { get; set; }
public UnmappedMemberPolicy UnmappedSourceMembers { get; set; }
public NullableMismatchPolicy NullableMismatch { get; set; }
public NullCollectionStrategy NullCollections { get; set; }
}Assembly defaults MUST be limited to stable cross-cutting behavior. Numeric conversion, explicit operators, enum mapping, reference handling, and patch behavior MUST NOT be assembly-level options in 1.0.
namespace Mammoth.LiteMapper;
[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Assembly,
AllowMultiple = true,
Inherited = false)]
public sealed class UseMapperAttribute : Attribute
{
public UseMapperAttribute(Type mapperType)
{
MapperType = mapperType;
}
public Type MapperType { get; }
}A referenced type MAY contain generated mapping methods, handwritten mapping methods, [MappingConverter] methods, or any combination.
Externally registered mapper or converter-container types MUST be static in 1.0.
namespace Mammoth.LiteMapper;
[AttributeUsage(
AttributeTargets.Method,
AllowMultiple = false,
Inherited = false)]
public sealed class DefaultMappingAttribute : Attribute
{
}
[AttributeUsage(
AttributeTargets.Method,
AllowMultiple = false,
Inherited = false)]
public sealed class MappingConverterAttribute : Attribute
{
}
[AttributeUsage(
AttributeTargets.Constructor,
AllowMultiple = false,
Inherited = false)]
public sealed class MappingConstructorAttribute : Attribute
{
}MappingConstructorAttribute is the only LiteMapper attribute permitted on source or destination model declarations in 1.0.
namespace Mammoth.LiteMapper;
[AttributeUsage(
AttributeTargets.Method,
AllowMultiple = true,
Inherited = false)]
public sealed class MapPropertyAttribute : Attribute
{
public string? Source { get; set; }
public string? Target { get; set; }
public string? Use { get; set; }
public Type? ConverterType { get; set; }
}Rules:
TargetMUST be supplied and MUST identify one direct target member.TargetMUST NOT contain a dot.SourceMAY be omitted only whenUseidentifies a converter that consumes the complete root source object.- When
Sourceis present, it MAY be a dotted source member path. SourceMUST NOT contain method-call syntax.Usenames a converter or mapping method.ConverterTypeis required whenUseselects an external method not available through normal local or registered resolution.- The generator MUST validate all strings to exact Roslyn symbols and report errors at the corresponding attribute argument when possible.
Example:
[MapProperty(
Source = "Email.Value",
Target = nameof(CustomerDto.Email),
Use = nameof(MapEmail))]Root-source example:
[MapProperty(
Target = nameof(CustomerDto.FullName),
Use = nameof(MapFullName))]namespace Mammoth.LiteMapper;
[AttributeUsage(
AttributeTargets.Method,
AllowMultiple = true,
Inherited = false)]
public sealed class IgnoreTargetAttribute : Attribute
{
public IgnoreTargetAttribute(string member)
{
Member = member;
}
public string Member { get; }
}
[AttributeUsage(
AttributeTargets.Method,
AllowMultiple = true,
Inherited = false)]
public sealed class IgnoreSourceAttribute : Attribute
{
public IgnoreSourceAttribute(string member)
{
Member = member;
}
public string Member { get; }
}
[AttributeUsage(
AttributeTargets.Method,
AllowMultiple = true,
Inherited = false)]
public sealed class UseTargetDefaultAttribute : Attribute
{
public UseTargetDefaultAttribute(string member)
{
Member = member;
}
public string Member { get; }
}IgnoreSource excludes the named source member from strict source-member reporting. When effective UnmappedSourceMembers is Ignore, the attribute is still validated but has no additional runtime or generation effect.
UseTargetDefault MUST preserve a real declared target default, such as:
- a property or field initializer that remains active when the member is not assigned;
- an optional constructor parameter default used by the selected constructor.
When a selected constructor parameter corresponds to the named target member under the effective name-matching policy, UseTargetDefault instructs LiteMapper to omit that optional argument and use its declared default.
If no real target default exists, generation MUST fail. LiteMapper MUST NOT generate default!.
namespace Mammoth.LiteMapper;
public sealed class LiteMapperCycleException : InvalidOperationException
{
public LiteMapperCycleException(
Type sourceType,
Type destinationType,
string mappingMethod,
string memberPath)
: base(CreateMessage(
sourceType,
destinationType,
mappingMethod,
memberPath))
{
SourceType = sourceType;
DestinationType = destinationType;
MappingMethod = mappingMethod;
MemberPath = memberPath;
}
public Type SourceType { get; }
public Type DestinationType { get; }
public string MappingMethod { get; }
public string MemberPath { get; }
private static string CreateMessage(
Type sourceType,
Type destinationType,
string mappingMethod,
string memberPath)
{
// Exact punctuation is not public API, but all values must be present.
return $"A mapping cycle was detected while mapping " +
$"'{sourceType}' to '{destinationType}' in " +
$"'{mappingMethod}' at '{memberPath}'.";
}
}The public constructor signature, exposed properties, base class, and namespace are part of the public API. The exact punctuation of the generated message is not public API, but the message MUST contain all four values. The exception MUST NOT retain the source object or arbitrary object graph.
Effective configuration MUST be resolved in this order:
- method-level
MappingOptionsAttributevalue when notUnspecified; - mapper-level
LiteMapperAttributevalue when notUnspecified; - assembly-level
LiteMapperDefaultsAttributevalue when available and notUnspecified; - library default.
For method-level Boolean options:
Enabledresolves totrue;Disabledresolves tofalse;Unspecifiedfalls through to mapper-level Boolean configuration.
The 1.0 library defaults MUST be:
| Option | Default |
|---|---|
| Name matching | ExactThenIgnoreCase |
| Ordinary writable nullable/value target member | Warning |
| Unmapped source member | Ignore |
| Nullable mismatch | Error |
| Null collection | contextual, as defined below |
| Numeric conversion | ImplicitOnly |
| Explicit operators | disabled |
| Enum mapping | ByName |
| Enum numeric conversion | Checked |
| Unmatched enum values | Error |
| Reference handling | None |
| Ignore null source members | disabled |
If no explicit null-collection strategy is configured:
- a nullable source collection to a nullable target collection MUST preserve null;
- a nullable source collection to a non-null target collection MUST produce a compile-time error;
- a non-null source collection MUST map normally.
Explicit strategies behave as follows:
Error: nullable source collection is rejected unless null is already impossible by flow/type contract;Preserve: null is assigned only when the target type is nullable; otherwise generation fails;Empty: null produces an empty destination collection.
Target members MUST be evaluated independently rather than classifying an entire destination type as mutable or immutable.
Regardless of the ordinary target-member policy:
- an unsatisfied
requiredmember is an error; - an unassigned non-nullable reference member with no constructor binding or approved target default is an error;
- a member that cannot be assigned after construction and is not constructor-bound or covered by
UseTargetDefaultis an error when leaving it uninitialized would violate its declared contract; - ordinary writable value-type members, nullable reference members, and members with an explicitly approved target default follow the configured target-member policy.
Some conditions remain errors regardless of configurable unmapped-member severity, including:
- unsatisfied C#
requiredmembers; - inaccessible construction;
- nullable-to-non-nullable assignment under
Errorpolicy; - invalid mapping signatures;
- unsupported generated types;
- ambiguous constructor, converter, member, or mapping selection;
- invalid attribute configuration;
- missing target default requested through
UseTargetDefault.
A mapper MUST be either:
- a static partial class; or
- a non-abstract instance partial class.
The mapper MAY be sealed.
The following mapper declarations are unsupported:
- interfaces;
- structs;
- records used as mapper containers;
- abstract classes;
- generic mapper classes;
- mappers nested inside generic containing types.
A mapper MAY be nested in a non-generic containing type.
Every containing type in the nesting chain MUST be partial so generated source can reproduce the declaration chain.
LiteMapper MUST NOT relocate a nested mapper to an external helper type.
Instance mapper classes MAY use ordinary C# inheritance.
Generated mapping discovery MUST be scoped to the current mapper declaration and its explicitly registered static mapper/container types. LiteMapper MUST NOT implement inherited mapping-profile composition.
Ordinary inherited handwritten methods MAY be used when normal C# accessibility and resolution permit them, but their presence MUST NOT create implicit mapper configuration inheritance.
Instance mappers MAY use fields, properties, constructor-injected dependencies, and instance converter methods.
LiteMapper provides no thread-safety guarantee for user-owned state. Static generated mappers MUST contain no mutable generated state and MUST be safe for concurrent use.
A mapper MAY contain:
- generated bodyless partial mapping methods;
- handwritten mapping methods;
- helper methods;
- converter methods;
- unrelated members.
The generator MUST only implement eligible bodyless partial methods. It MUST NOT rewrite or duplicate handwritten method bodies.
A new-object mapping method MUST:
- be a bodyless partial method;
- have exactly one source parameter;
- return a non-
voiddestination type; - be synchronous;
- use an unmodified by-value source parameter;
- use
public,internal, orprivateaccessibility; - be static when declared in a static mapper;
- be an instance method or static method when declared in an instance mapper.
Examples:
[LiteMapper]
public static partial class CustomerMapper
{
public static partial CustomerDto ToDto(
this Customer source);
internal static partial CustomerSummary ToSummary(
Customer source);
private static partial AddressDto MapAddress(
Address source);
}Instance mapper:
[LiteMapper]
public sealed partial class ProductMapper
{
public partial ProductDto ToDto(Product source);
}Extension methods MUST NOT be declared in instance mapper classes because C# requires extension methods to be static members of static classes.
Supported reference-target signatures are:
public static partial void ApplyTo(
Source source,
Destination destination);public static partial Destination ApplyTo(
Source source,
Destination destination);For a non-null reference destination, the destination-returning form MUST return the same destination reference.
A nullable destination parameter is invalid for a void update because creation or replacement could not be observed.
A nullable reference destination MAY be accepted by a destination-returning update method when:
- the method returns a non-null destination type;
- LiteMapper can construct a new destination using the normal constructor algorithm;
- a null input destination causes creation and mapping of a new destination;
- a non-null input destination is updated and returned.
Existing-target mapping into a value type MUST use ref:
public static partial void ApplyTo(
Source source,
ref Destination destination);A destination-returning value-type update without ref is not supported in 1.0. Implementation MUST NOT claim mutation of a struct passed by value.
The generator MUST reject:
asyncmethods;Task<T>orValueTask<T>mapping return conventions;- source parameters using
in,ref, oroutfor new-object mapping; - arbitrary additional context parameters;
- generic mapping methods;
- open generic source or destination declarations;
- top-level array update methods;
- bodyless partial methods whose shape cannot be classified unambiguously.
LiteMapper MAY allow multiple extension methods with the same name and source type in different mapper classes. Normal C# overload and namespace ambiguity rules apply. The generator MUST NOT attempt compilation-wide extension-method policing.
Automatic mapping MUST consider:
- public instance properties with accessible public getters on the source;
- public instance fields on the source;
- public instance properties with a construction-time or update-time legal assignment path on the target;
- public instance fields on the target when assignment is legal;
- accessible public inherited members.
Automatic mapping MUST exclude:
- static members;
- constants;
- events;
- indexers;
- compiler-generated backing fields;
- non-public members;
- source methods;
- destination methods.
9.2 Inheritance and hidden members
When inherited and declared members share a name:
- the most-derived accessible member wins;
- when a property and field are otherwise tied, the property wins;
- LiteMapper MUST emit a warning that a hidden member was selected.
Exact:
- uses ordinal case-sensitive equality only.
ExactThenIgnoreCase:
- attempts exact ordinal equality;
- when no exact match exists, attempts a unique ordinal case-insensitive match;
- reports ambiguity when multiple candidates match case-insensitively.
IgnoreCase:
- performs unique ordinal case-insensitive matching without exact-match priority;
- reports ambiguity when multiple candidates match.
LiteMapper MUST NOT normalize underscores, prefixes, suffixes, acronyms, or PascalCase segments.
MapProperty.Source MAY contain a dotted path such as:
Address.Country.Code
Each path segment MUST resolve to a readable public property or field.
The generator MUST:
- validate every segment at compile time;
- report the exact failing segment;
- evaluate each path segment no more than once when repeated evaluation could occur;
- generate temporaries for nullable traversal or repeated use;
- preserve exceptions thrown by getters;
- apply nullability policy at each segment.
Source paths MUST NOT contain:
- method calls;
- indexer access;
- array indexing;
- conditional syntax;
- target references;
- arbitrary C# expressions.
Target names MUST identify direct members. Dotted target paths are unsupported in 1.0.
Source methods do not participate automatically and cannot appear in source path syntax.
A source method MAY be used inside a handwritten root-source converter:
[MapProperty(
Target = nameof(CustomerDto.DisplayName),
Use = nameof(MapDisplayName))]
private static string MapDisplayName(Customer source) =>
source.GetDisplayName();Multiple attributes or conventions assigning the same target member MUST produce a fatal diagnostic unless one is merely the automatically discovered candidate replaced by one explicit configuration.
Attribute order MUST NOT select a winner.
A constructor is eligible when it is accessible from the generated mapper code under ordinary C# accessibility rules.
Record copy constructors MUST be excluded from automatic selection.
Private constructors MUST NOT be invoked unless ordinary C# accessibility from the generated mapper makes the call legal, such as a mapper nested within the same declaring type. Reflection is forbidden.
For a new-object mapping, LiteMapper MUST apply this algorithm:
- Find eligible constructors marked
[MappingConstructor]. - If more than one eligible constructor is marked, report a fatal diagnostic.
- If exactly one eligible constructor is marked, select it if every required parameter is satisfiable.
- Otherwise collect eligible, fully satisfiable parameterized constructors.
- If exactly one fully satisfiable parameterized constructor exists, select it.
- If multiple exist, select the unique constructor with the greatest number of parameters.
- If multiple constructors remain tied, report ambiguity.
- If no parameterized constructor is selected, use an eligible parameterless constructor and legal member initialization.
- If no construction path exists, report a fatal diagnostic.
An optional constructor parameter is satisfiable using its declared default when no source/configuration maps it.
Constructor parameters use the effective name-matching policy.
Exact matching is attempted first under ExactThenIgnoreCase; a unique case-insensitive fallback is permitted.
A parameter MAY be satisfied by:
- explicit member configuration;
- automatic source-member matching;
- a converter;
- an optional parameter default;
- another conversion supported by the normal conversion pipeline.
A target member bound through the selected constructor MUST NOT be assigned again by default.
LiteMapper 1.0 has no declarative option to force duplicate assignment. A user requiring setter execution after construction MUST write the mapping manually.
- Records and record structs MUST be supported.
initproperties MAY be assigned during new-object creation through an object initializer.initproperties MUST NOT be assigned during existing-target updates.- Every C#
requiredmember MUST be satisfied by the selected constructor according to language metadata or by generated object initialization. - A constructor annotated with
SetsRequiredMembersAttributeMUST be treated according to the C# language contract. - LiteMapper MUST NOT assign
default!to satisfy a required member.
- A read-only target property or field MAY be populated only through a selected constructor.
- A
readonlydestination field MAY be constructor-bound. - An unbound required read-only member is a fatal error.
- An ordinary unbound read-only member is reported according to target-member policy, but it MUST NOT be assigned illegally.
A target initializer is not automatically considered a deliberate mapping default.
To preserve it intentionally, the mapping method MUST declare:
[UseTargetDefault(nameof(Target.Member))]Without UseTargetDefault, the member is analyzed as any other target member.
For each target value, LiteMapper MUST resolve mapping in this exact order:
- explicit
MapProperty.Useselection; - explicit source path selection, completing immediately when directly assignable and otherwise continuing through the remaining conversion stages with the selected source value;
- local method marked
[MappingConverter]; - local
Map{TargetMember}member converter; - explicit local mapping method;
- explicitly registered external converter;
- explicitly registered external mapping method;
- mapping marked
[DefaultMapping]for the source/destination pair; - identity or implicit C# conversion;
- enabled explicit operator or numeric conversion policy;
- enum conversion policy;
- collection mapping;
- generated private structural nested mapping;
- diagnostic for unresolved or ambiguous mapping.
Explicit user intent MUST outrank language and structural convenience.
Multiple mappings with the same source and destination types are allowed.
Exactly one MAY be marked [DefaultMapping] within a visible resolution scope.
When nested mapping requires a source/destination pair:
- use the unique default mapping when present;
- use a unique mapping when only one visible mapping exists;
- report ambiguity when multiple mappings exist and none is the unique default.
LiteMapper MUST NOT select arbitrary compatible helper methods solely by signature.
Eligible custom converters are:
- explicitly selected methods;
- methods marked
[MappingConverter]; - methods named
Map{TargetMember}for a specific target member; - explicit mapping methods used according to the resolution hierarchy.
A converter MUST:
- be synchronous;
- be non-generic;
- have exactly one source parameter;
- return a value assignable or validly convertible to the required target type;
- obey nullable-target rules.
Static mapper classes may use static converters only.
Instance mapper classes may use static or instance converters and MAY access their own fields and dependencies.
A converter selected for a normal source member receives that member value.
A converter selected by a MapProperty with omitted Source receives the complete root source object.
When Use names an overloaded method, LiteMapper MUST select a unique overload using the expected source value type and required target type. Identity parameter compatibility outranks implicit parameter compatibility. Equal candidates are ambiguous.
Exceptions thrown by converters MUST propagate unchanged. LiteMapper MUST NOT wrap them in a general mapping exception.
Class-level [UseMapper] registrations take precedence over assembly-level registrations when both expose otherwise equal candidates.
Every external registered type MUST be static.
LiteMapper MUST validate that a referenced type exists and exposes usable methods. It MUST NOT instantiate an external mapper or resolve it from dependency injection.
For a non-collection reference type mapping T -> T:
- use an explicit declared mapping when one resolves under the normal precedence;
- otherwise assign the same reference.
LiteMapper MUST NOT automatically deep-copy identical reference types.
Mutable collections are a deliberate exception and MUST be copied.
LiteMapper MUST support normal C# identity and implicit conversions, including:
- identity conversion;
- implicit reference conversion;
- boxing where legal;
- implicit numeric conversions;
- nullable lifting where semantically valid;
- implicit user-defined operators selected by normal C# overload resolution.
Explicit user-defined operators are disabled by default.
When enabled, LiteMapper MUST emit a normal explicit cast and use C# overload-resolution semantics.
If multiple explicit conversions are ambiguous, LiteMapper SHOULD report a diagnostic at the mapping member rather than relying only on a downstream compiler error.
ImplicitOnly:
- permits only identity and implicit numeric conversion;
- rejects narrowing conversions.
Checked:
- permits explicit numeric conversions in a checked context;
- overflow MUST throw the standard
OverflowException.
Unchecked:
- permits explicit numeric conversions in an unchecked context.
String parsing, formatting, Parse, TryParse, ToString, culture-dependent conversion, date/string conversion, and Guid/string conversion are not automatic and require a custom converter.
For a potentially null source value mapped to a non-null target:
Error:
- reports a compile-time error;
- omits the invalid implementation;
- never emits a null-forgiving operator merely to silence diagnostics.
Throw:
- emits a runtime null check;
- throws an appropriate standard exception, normally
ArgumentNullExceptionfor a root source andInvalidOperationExceptionfor an invalid nested/member value unless a more specific normal C# exception is appropriate; - MUST preserve the member path in the exception message.
Nullable reference type annotations that are oblivious because nullable context is disabled MUST not produce nullable mismatch diagnostics.
For a non-null source parameter, generated code MUST NOT emit a runtime null guard by default.
When the effective GuardNonNullSource option is enabled for a mapping method, generated code MUST reject a runtime null source value using ArgumentNullException.
For a nullable source parameter and nullable return type, null source MUST return null.
A nullable source parameter with a non-null return MUST follow effective nullable-mismatch policy.
An emitted root-source null check MAY use ArgumentNullException.ThrowIfNull only when that exact API exists in the consumer compilation; otherwise it MUST emit a portable explicit check.
By-name mapping MUST match declared enum member names exactly.
The effective UnmatchedEnumValuePolicy controls declared source members that have no target name:
Error: generation fails when any required declared source member lacks a target name;Throw: generation succeeds and the generated switch throwsArgumentOutOfRangeExceptionwhen an unmatched declared or unknown runtime value is encountered;ByValue: an unmatched declared name falls back to the enum numeric-conversion policy; overflow remains an error or runtime overflow according to that policy.
The library default is Error.
Even when every declared member is mapped, generated code MUST include a runtime default case that throws ArgumentOutOfRangeException for unknown numeric values not represented by a valid mapping.
By-value mapping MUST convert the underlying numeric value according to EnumNumericConversion.
Checkeduses a checked underlying conversion and throws the standardOverflowExceptionat runtime for an out-of-range runtime value.Uncheckeduses an unchecked underlying conversion.
The library default is Checked.
Enum numeric conversion is independent from general NumericConversion configuration.
For by-name mapping, a declared zero-valued source member MUST have a matching target member.
LiteMapper MUST NOT assume all zero values map to target numeric zero when name mapping was requested.
For [Flags] enums mapped by name:
- every atomic non-zero source flag MUST have a matching target flag;
- declared composite aliases need not have matching names when their atomic components all map consistently;
- zero is handled by the zero-member rule;
- runtime values MUST be decomposed and reconstructed without losing unmatched bits;
- values containing unknown bits MUST throw.
Source aliases sharing one numeric value are allowed only when every alias resolves to target members with the same target numeric value.
Target aliases are allowed when name mapping selects one unique target member name, even if multiple target names share the same numeric value.
When source and target member types require structural object mapping and no higher-precedence mapping exists, LiteMapper MUST generate a private nested helper.
Automatic helpers MUST:
- remain private implementation details;
- use exact closed source and destination types;
- never introduce generic helper methods or open generic templates;
- reuse an explicit or default mapping when one is visible;
- participate in recursive type analysis;
- receive and forward cycle-tracking state only when required.
A closed constructed type such as:
PagedResult<Customer> -> PagedResultDto<CustomerDto>
is treated as an ordinary concrete type pair.
LiteMapper MUST NOT infer or cache a reusable open generic mapping template.
Nested null behavior follows the same source/target nullability policy as scalar members.
For nullable target members, a null nested source maps to null.
For non-null targets, the default policy is a compile-time error unless the user chooses Throw or supplies a converter/fallback through ordinary code.
For a writable nested target property during update:
- replacement with a newly mapped nested object is the default;
- mutation of the existing nested object is permitted only when an explicit compatible existing-target nested mapping method is declared and selected.
For a get-only nested target object:
- automatic update is permitted only when an explicit compatible existing-target nested mapping method is declared;
- otherwise generation fails.
LiteMapper MUST NOT infer nested mutation from the mere presence of a non-null object.
LiteMapper 1.0 MUST support automatic element mapping for:
- one-dimensional arrays;
- jagged arrays;
IEnumerable<T>;ICollection<T>;IList<T>;IReadOnlyCollection<T>;IReadOnlyList<T>;List<T>;ISet<T>;HashSet<T>;IReadOnlySet<T>when that symbol exists in the consumer compilation;Dictionary<TKey, TValue>;IDictionary<TKey, TValue>;IReadOnlyDictionary<TKey, TValue>.
LiteMapper 1.0 MUST reject automatic mapping for:
- rectangular arrays such as
T[,]; - queues and stacks;
- immutable collections;
- arbitrary custom collection types;
IAsyncEnumerable<T>;Span<T>andReadOnlySpan<T>;- collection builders requiring runtime discovery.
A public top-level collection mapping method MUST be explicitly declared.
LiteMapper MUST NOT automatically generate public ToList, ToArray, or pluralized extension methods for every element mapping.
Private collection helpers required by nested member mapping MAY be generated automatically.
When a target is an interface, LiteMapper MUST use these defaults:
| Target | Concrete generated result |
|---|---|
IEnumerable<T> |
T[] |
IReadOnlyCollection<T> |
T[] |
IReadOnlyList<T> |
T[] |
ICollection<T> |
List<T> |
IList<T> |
List<T> |
ISet<T> |
HashSet<T> |
IReadOnlySet<T> |
HashSet<T> |
IDictionary<TKey,TValue> |
Dictionary<TKey,TValue> |
IReadOnlyDictionary<TKey,TValue> |
Dictionary<TKey,TValue> |
Read-only static interfaces do not guarantee immutable runtime implementations. LiteMapper MUST NOT add wrappers merely to simulate deep immutability.
LiteMapper MUST preserve source enumeration order when the destination shape is ordered.
Sets and dictionaries follow their container semantics.
Dictionary insertion/enumeration order is preserved only to the extent provided by the selected runtime implementation. LiteMapper does not establish a stronger cross-runtime guarantee.
A source enumerable MUST be enumerated exactly once unless it exposes indexed access or another direct path that avoids enumeration.
LiteMapper MUST NOT call Count() on an arbitrary enumerable merely to preallocate capacity.
Capacity SHOULD be preallocated when count is cheaply available from:
- arrays;
ICollection<T>;IReadOnlyCollection<T>;- known concrete collections;
- another symbol-proven constant-time count API.
Mutable source collections MUST be copied even when source and target collection types and element types are identical.
LiteMapper MUST NOT alias a mutable collection into a destination object.
Known immutable/value-like collection types are outside 1.0 automatic support.
Every source element MUST produce exactly one target element for sequence and array mappings.
Null elements MUST NOT be dropped or replaced silently. Nullable-to-non-nullable element mapping follows the scalar nullable mismatch policy.
Jagged arrays are mapped recursively and preserve each nested array length.
Set mappings follow target comparer semantics. Duplicate mapped values MAY collapse naturally.
When source and destination element mapping is identity-compatible and the source comparer is type-compatible, LiteMapper SHOULD preserve the comparer.
Otherwise LiteMapper MUST use the destination's default comparer.
Dictionary keys and values MUST independently use the complete conversion-resolution pipeline.
When key conversion is identity-compatible and the source comparer is type-compatible, LiteMapper SHOULD preserve the comparer. Otherwise it MUST use the destination's default comparer.
Entries MUST be inserted with normal Add semantics. If multiple source keys map to one equal target key, the destination dictionary MUST throw rather than silently use first-wins or last-wins behavior.
Null source collections obey the effective null-collection strategy defined in section 6.
For update mappings with IgnoreNullSourceMembers enabled, a null collection source member skips the assignment and leaves the destination member unchanged, including when null is encountered through a configured source path.
For a writable collection property during update:
- LiteMapper MUST create a new mapped collection and replace the target value.
LiteMapper MUST NOT automatically:
- clear and repopulate a get-only collection;
- merge by index;
- merge by key;
- retain existing elements;
- partially update an array.
A get-only collection target is unsupported for generated update mapping. A user requiring population semantics must implement that mapping manually.
Top-level array update methods are unsupported. A normal new-object collection mapping may return a replacement array.
Writable scalar members are assigned after conversion using the normal resolution pipeline.
init members, read-only fields, and inaccessible setters cannot be updated.
For a non-null destination parameter, a runtime null value MUST throw ArgumentNullException.
For a nullable destination in a destination-returning method, LiteMapper MUST create a replacement destination when construction is possible and return it.
When enabled for an existing-target mapping:
- any null direct source member skips assignment;
- a null encountered at any segment in a configured source path skips assignment;
- the destination member remains unchanged;
- non-null values map normally;
- the behavior applies to scalars, nested objects, and collections.
It MUST NOT apply to new-object mappings. Configuring it on a new-object mapping SHOULD produce a diagnostic.
For reference targets, a destination-returning update MUST return the same input reference unless the input was nullable and null, in which case it returns the newly created destination.
For struct targets, the required mutation form is ref.
Recursive type mappings are supported.
With ReferenceHandling.None:
- finite runtime trees map recursively;
- cyclic runtime object graphs may result in stack overflow;
- no tracker allocation is permitted.
The generator SHOULD emit an informational diagnostic when a recursive type graph uses None, making the runtime risk visible without rejecting valid finite trees.
With ReferenceHandling.ThrowOnCycle:
- the generator MUST detect recursive mapping components at compile time;
- only mapping paths within recursive strongly connected components require tracking;
- one tracking state MUST be created at the public mapping entry point for a recursive mapping call;
- the same tracker MUST be forwarded through nested helpers and collection element mappings;
- non-recursive public mappings MUST NOT allocate a tracker.
Cycle detection MUST use reference identity, not Equals.
The tracker MUST represent the active recursion path:
- before recursively descending into a tracked source object, attempt to add that exact reference;
- if the reference is already active, throw
LiteMapperCycleException; - after the recursive mapping finishes, remove the reference in a
finallyblock.
A source reference appearing in two separate non-overlapping branches is not a cycle. It MUST be mapped independently in each branch.
Only reference-type instances participate in cycle tracking. Value types are copied normally and MUST NOT be boxed merely for tracking.
LiteMapperCycleException MUST expose:
- source type;
- destination type;
- public mapping method name;
- member path at which the cycle was detected.
The message SHOULD include all four values in a deterministic, readable format.
The exception MUST NOT retain the offending source instance.
The tracker MAY be:
- emitted as a private mapper-specific helper; or
- provided by an internal shared runtime helper.
The choice MUST be based on benchmarks and package constraints and MUST NOT alter public API, allocation guarantees, path semantics, or thread safety.
An interface MAY be a source type. LiteMapper maps its accessible public properties and fields from the declared interface contract.
Default interface property declarations participate normally. Default interface methods do not become automatic source members.
An interface or abstract class MUST NOT be an automatically constructed destination. A handwritten mapping or explicit converter must return a concrete implementation.
A concrete type may map to object using normal implicit conversion.
object to a concrete destination requires an explicitly selected custom converter. LiteMapper MUST NOT generate runtime type dispatch.
dynamic is unsupported for generated mapping.
Automatic structural mapping is unsupported for:
ref structtypes;Span<T>;ReadOnlySpan<T>;- pointer types;
- function pointers.
A handwritten converter or handwritten mapping method MAY use these types when legal C# allows it. LiteMapper does not generate the unsafe or ref-like behavior.
Tuple-to-tuple mapping is supported when the source and destination tuple arity is equal and each element at the same ordinal position can be converted under the normal pipeline. Tuple element names do not change positional mapping semantics.
Tuple-to-object and object-to-tuple structural mapping are unsupported in 1.0.
Anonymous types receive no explicit support.
The generator MUST implement IIncrementalGenerator directly.
It MUST NOT implement the deprecated classic ISourceGenerator pipeline as its primary architecture.
The generator instance MUST be stateless. It MUST NOT store compilation state in instance fields.
The pipeline MUST be partitioned so changing one mapper regenerates only that mapper and any mappings whose relevant model/configuration inputs changed, where possible.
A change to assembly-wide defaults or assembly-wide external mapper registrations MAY invalidate all affected mappers.
Incrementality MUST be tested through observable tracked generator steps, not assumed from the interface name.
Mapper discovery SHOULD use attribute-targeted syntax filtering, such as SyntaxProvider.ForAttributeWithMetadataName, or an equivalent precise incremental pipeline.
The pipeline MUST avoid scanning every syntax node semantically when a cheaper syntax predicate can eliminate it.
The generator MUST build an immutable capability model from:
- effective C# language version;
- preprocessor symbols;
- required framework types and members;
- nullable context and annotations;
- analyzer/MSBuild options;
- relevant compilation references.
Changing language version, relevant symbols, or relevant analyzer options MUST invalidate affected generated output.
Generated source MUST contain:
// <auto-generated/>It MUST enable nullable analysis when supported and appropriate.
It MAY use targeted warning pragmas only for warnings generated by mechanically safe code. It MUST NOT globally suppress consumer warnings or suppress semantic problems.
Generated declarations SHOULD carry deterministic GeneratedCodeAttribute metadata containing the LiteMapper generator version.
Version metadata MUST NOT include timestamps or machine information.
User-authored XML documentation on partial method declarations remains the documentation for the public method.
The generator MUST NOT hide user-declared public mapping methods from IntelliSense.
Generated private helpers remain private.
LiteMapper MUST emit one generated source file per mapper class.
Hint names MUST be based on the fully qualified metadata name plus a stable deterministic hash, for example:
Mammoth.LiteMapper.MyNamespace.CustomerMapper.A1B2C3D4.g.cs
The hash algorithm and normalized input MUST be stable within a generator version.
Generated members, mappings, diagnostics, and helpers MUST be ordered deterministically by stable symbol identity rather than syntax-tree enumeration accidents.
Generated code MAY use newer framework APIs only when the exact symbols and signatures exist.
Examples include:
ArgumentNullException.ThrowIfNull;- collection constructors accepting capacity;
- target-specific collection helpers.
A portable fallback MUST exist for every required supported consumer target.
Generated code MUST NOT depend on:
- runtime reflection;
dynamic;- expression compilation;
- runtime mapping registries;
- service locators;
- assembly scanning;
- emitted IL;
System.Linqfor hot collection loops when direct loops are clearer and more allocation-efficient.
Diagnostic IDs are public and stable immediately when introduced.
The prefix is LITEMAPPER.
Category ranges are:
| Range | Category |
|---|---|
LITEMAPPER0xxx |
mapper declarations, configuration, generator infrastructure |
LITEMAPPER1xxx |
members, paths, constructors, object creation |
LITEMAPPER2xxx |
nullability and conversions |
LITEMAPPER3xxx |
mapping and nested-resolution ambiguity |
LITEMAPPER4xxx |
collections and dictionaries |
LITEMAPPER5xxx |
existing-target mappings |
LITEMAPPER6xxx |
recursive graphs and cycle configuration |
LITEMAPPER7xxx |
enum mapping |
LITEMAPPER9xxx |
internal generator failures |
Standard .editorconfig severity overrides MUST work for configurable diagnostics.
Fatal semantic diagnostics MUST use WellKnownDiagnosticTags.NotConfigurable or equivalent behavior so suppressing them cannot create an invalid missing implementation contract.
Messages SHOULD use concise type names. Diagnostic properties SHOULD contain fully qualified metadata names. When concise names are ambiguous in the message, fully qualified names MUST be used.
Location selection priority is:
- exact relevant attribute argument or model member;
- mapping method;
- mapper class.
The implementation MUST define and test at least the following stable diagnostics.
| ID | Default severity | Name | Meaning |
|---|---|---|---|
LITEMAPPER0001 |
Error, non-configurable | MapperMustBePartial | Mapper or required containing type is not partial. |
LITEMAPPER0002 |
Error, non-configurable | UnsupportedMapperType | Mapper is an interface, struct, record container, or another unsupported kind. |
LITEMAPPER0003 |
Error, non-configurable | GenericMapperNotSupported | Mapper or containing type is generic. |
LITEMAPPER0004 |
Error, non-configurable | AbstractMapperNotSupported | Instance mapper is abstract. |
LITEMAPPER0005 |
Error, non-configurable | InvalidMappingMethodSignature | Partial method cannot be classified as a supported mapping. |
LITEMAPPER0006 |
Error, non-configurable | AsyncMappingNotSupported | Mapping method or converter is asynchronous. |
LITEMAPPER0007 |
Error, non-configurable | UnsupportedMappingAccessibility | Mapping accessibility is unsupported. |
LITEMAPPER0008 |
Error, non-configurable | InvalidMapperConfiguration | Mapper-wide configuration contains an invalid value or combination. |
LITEMAPPER0009 |
Error, non-configurable | InvalidMethodConfiguration | Method configuration contains an invalid value or combination. |
LITEMAPPER0010 |
Error, non-configurable | InvalidUseMapperType | Registered external type is missing, invalid, or exposes no usable methods. |
LITEMAPPER0011 |
Error, non-configurable | ExternalMapperMustBeStatic | External registered mapper/container is not static. |
LITEMAPPER0012 |
Error, non-configurable | UnsupportedLanguageVersion | Consumer language version is older than C# 9. |
LITEMAPPER0013 |
Error, non-configurable | DuplicateConfiguration | A non-repeatable configuration is declared more than once. |
| ID | Default severity | Name | Meaning |
|---|---|---|---|
LITEMAPPER1001 |
Warning, configurable | UnmappedTargetMember | Ordinary target member is not mapped. |
LITEMAPPER1002 |
Error, non-configurable | RequiredTargetMemberNotMapped | Required or semantically mandatory target member is unsatisfied. |
LITEMAPPER1003 |
Ignore, configurable | UnmappedSourceMember | Source member is unused while strict source checking is enabled. |
LITEMAPPER1004 |
Error, non-configurable | AmbiguousMemberMatch | Multiple source members match one target member. |
LITEMAPPER1005 |
Warning | HiddenMemberSelected | Most-derived hidden member was selected. |
LITEMAPPER1006 |
Error, non-configurable | InvalidSourcePath | Source path or one segment cannot be resolved. |
LITEMAPPER1007 |
Error, non-configurable | TargetPathNotSupported | Target contains a dotted path. |
LITEMAPPER1008 |
Error, non-configurable | DuplicateTargetMapping | Multiple configurations assign the same target member. |
LITEMAPPER1009 |
Error, non-configurable | TargetMemberNotWritable | Target member cannot be assigned in this mapping mode. |
LITEMAPPER1010 |
Error, non-configurable | ConstructorNotFound | No legal target construction path exists. |
LITEMAPPER1011 |
Error, non-configurable | AmbiguousConstructor | Constructor-selection algorithm has a tie. |
LITEMAPPER1012 |
Error, non-configurable | MultipleMappingConstructors | More than one eligible constructor is marked. |
LITEMAPPER1013 |
Error, non-configurable | MappingConstructorNotSatisfiable | Marked constructor cannot be satisfied. |
LITEMAPPER1014 |
Error, non-configurable | TargetDefaultMissing | UseTargetDefault names a member with no real default. |
LITEMAPPER1015 |
Error, non-configurable | InvalidIgnoredMember | Ignore attribute names a missing or invalid member. |
LITEMAPPER1016 |
Error, non-configurable | IndexerNotSupported | Configuration attempts to map an indexer. |
| ID | Default severity | Name | Meaning |
|---|---|---|---|
LITEMAPPER2001 |
Error, configurable only to documented policy | NullableToNonNullable | Potential null maps to a non-null target. |
LITEMAPPER2002 |
Error, non-configurable | InvalidNullCollectionMapping | Null collection strategy cannot satisfy target nullability. |
LITEMAPPER2003 |
Error, non-configurable | NullableElementMismatch | Nullable collection element maps to a non-null element target. |
LITEMAPPER2004 |
Error, non-configurable | ConversionNotFound | No valid conversion or mapping exists. |
LITEMAPPER2005 |
Error, non-configurable | AmbiguousConversion | Multiple conversion candidates have equal precedence. |
LITEMAPPER2006 |
Error | ExplicitOperatorDisabled | Mapping requires an explicit operator that is disabled. |
LITEMAPPER2007 |
Error | NarrowingNumericConversionDisabled | Mapping requires narrowing numeric conversion under ImplicitOnly. |
LITEMAPPER2008 |
Error, non-configurable | UnsupportedGeneratedType | Type requires dynamic, pointer, ref-like, or another unsupported generated behavior. |
LITEMAPPER2009 |
Error, non-configurable | InvalidConverterSignature | Selected converter has an unsupported signature. |
LITEMAPPER2010 |
Error, non-configurable | ConverterNullabilityMismatch | Converter result cannot satisfy target nullability. |
LITEMAPPER2011 |
Error, non-configurable | AmbiguousConverter | Multiple converters have equal resolution precedence. |
LITEMAPPER2012 |
Error, non-configurable | RuntimeObjectDispatchNotSupported | object or abstract runtime dispatch would be required. |
| ID | Default severity | Name | Meaning |
|---|---|---|---|
LITEMAPPER3001 |
Error, non-configurable | AmbiguousMapping | Multiple mappings exist for a required type pair. |
LITEMAPPER3002 |
Error, non-configurable | DuplicateDefaultMapping | More than one default exists for a type pair. |
LITEMAPPER3003 |
Error, non-configurable | DefaultMappingNotUsable | Selected default mapping is inaccessible or incompatible. |
LITEMAPPER3004 |
Error, non-configurable | StructuralNestedMappingFailed | Automatic nested mapping cannot be generated. |
LITEMAPPER3005 |
Error, non-configurable | AbstractDestinationNotSupported | Destination cannot be constructed because it is abstract or an interface. |
| ID | Default severity | Name | Meaning |
|---|---|---|---|
LITEMAPPER4001 |
Error, non-configurable | UnsupportedCollectionShape | Collection shape is outside 1.0 support. |
LITEMAPPER4002 |
Error, non-configurable | RectangularArrayNotSupported | Mapping uses a multidimensional rectangular array. |
LITEMAPPER4003 |
Error, non-configurable | CollectionTargetCannotBeConstructed | No legal concrete destination collection can be created. |
LITEMAPPER4004 |
Error, non-configurable | GetOnlyCollectionUpdateNotSupported | Update targets a get-only collection. |
LITEMAPPER4005 |
Error, non-configurable | TopLevelArrayUpdateNotSupported | Existing-target array mapping was declared. |
LITEMAPPER4006 |
Error, non-configurable | CustomCollectionNotSupported | Custom collection convention would be required. |
| ID | Default severity | Name | Meaning |
|---|---|---|---|
LITEMAPPER5001 |
Error, non-configurable | InvalidUpdateSignature | Existing-target method has an unsupported signature. |
LITEMAPPER5002 |
Error, non-configurable | NullableVoidDestinationNotSupported | Nullable destination cannot be replaced through a void method. |
LITEMAPPER5003 |
Error, non-configurable | StructUpdateRequiresRef | Value-type destination is passed by value. |
LITEMAPPER5004 |
Error, non-configurable | InitOnlyMemberCannotBeUpdated | Update attempts to assign an init-only member. |
LITEMAPPER5005 |
Error, non-configurable | NestedUpdateMappingRequired | Get-only/existing nested mutation lacks explicit update mapping. |
LITEMAPPER5006 |
Warning | IgnoreNullOnlyValidForUpdate | Patch null-skipping was configured on a new-object mapping. |
| ID | Default severity | Name | Meaning |
|---|---|---|---|
LITEMAPPER6001 |
Info | RecursiveMappingWithoutCycleDetection | Recursive type graph uses ReferenceHandling.None. |
LITEMAPPER6002 |
Error, non-configurable | InvalidReferenceHandling | Reference handling is unsupported for the declared mapping. |
LITEMAPPER6003 |
Error, non-configurable | CyclePathCannotBeTracked | Required recursive path cannot use reference identity. |
| ID | Default severity | Name | Meaning |
|---|---|---|---|
LITEMAPPER7001 |
Error, non-configurable | EnumMemberNotMapped | Source enum member has no target mapping. |
LITEMAPPER7002 |
Error, non-configurable | EnumAliasConflict | Aliases map inconsistently. |
LITEMAPPER7003 |
Error, non-configurable | EnumZeroMemberNotMapped | Required zero member has no match. |
LITEMAPPER7004 |
Error, non-configurable | EnumFlagNotMapped | Atomic flag has no target match. |
LITEMAPPER7005 |
Error, non-configurable | EnumValueOverflow | By-value mapping cannot fit destination underlying type. |
| ID | Default severity | Name | Meaning |
|---|---|---|---|
LITEMAPPER9001 |
Error, non-configurable | InternalGeneratorFailure | Unexpected failure while processing one mapper. |
Unexpected exceptions MUST be caught per mapper where possible.
The generator MUST:
- emit one sanitized
LITEMAPPER9001at the mapper; - avoid including absolute paths or sensitive environment details;
- continue processing unrelated mapper classes;
- expose full exception detail only through generator-development/test configuration.
When mapper-wide configuration is invalid, every generated method in that mapper fails, while unrelated mapper classes continue.
When one mapping method has a fatal method-specific error, valid independent methods in the same mapper MUST still generate unless the invalid method corrupts shared mapper configuration.
The generator SHOULD separate these incremental stages:
- mapper syntax candidate discovery;
- mapper symbol validation;
- assembly defaults and assembly registrations;
- parse/language capability extraction;
- framework capability extraction;
- mapper configuration model;
- mapping method model creation;
- dependency graph and recursive-component analysis;
- source rendering;
- diagnostic emission.
Intermediate models used as incremental values MUST have structural equality based only on relevant semantic inputs.
The implementation SHOULD avoid retaining entire Compilation, SemanticModel, or syntax trees in final incremental models when a compact immutable representation suffices.
LiteMapper 1.0 MAY consume a small documented set of build properties:
LiteMapper_EmitDebugMetadata
LiteMapper_IncludeGeneratedSourceComments
LiteMapper_TreatInternalGeneratorErrorsAsExceptions
These options are intended for diagnostics, development, and tests. They MUST NOT alter mapping semantics.
Tests MUST prove:
- unchanged inputs are cached;
- changing one mapper does not regenerate unrelated mappers;
- changing a relevant model symbol regenerates affected mappings;
- changing an unrelated model does not regenerate unaffected mappings;
- language-version changes invalidate emitted source when capability selection changes;
- relevant analyzer-option changes invalidate only affected outputs;
- output ordering remains deterministic.
All standard test projects MUST use MSTest with built-in assertion APIs.
The repository MUST NOT require FluentAssertions, Shouldly, or another assertion library.
The test suite MUST use both:
- official Roslyn source-generator testing packages for diagnostics and source-generation cases;
- a custom compilation harness for incremental-step inspection, in-memory execution, capability matrices, and targeted source inspection.
Representative generated-source snapshots MUST be checked in as ordinary expected .g.cs files.
Snapshots MUST cover representative algorithms, not every combinatorial test case.
Snapshot frameworks are not required.
Runtime behavior MUST be tested through both:
- in-memory generated compilation and invocation, where practical;
- normally compiled fixture/consumer projects.
Reflection MAY be used by test infrastructure to invoke generated methods. The zero-reflection guarantee applies to shipped generated mapping paths.
Pull-request CI MUST test at least:
netstandard2.0;net8.0;net10.0.
Release CI MUST test:
netstandard2.0;net8.0;net9.0;net10.0.
CI MUST test:
- C# 9;
- the language version associated with the current supported LTS SDK used by the project;
latestsupported by the current CI SDK.
CI MUST test:
- Roslyn 4.8.0;
- the current stable Roslyn version pinned by the repository.
The generator package MUST reference Roslyn 4.8.0 for its compile-time API baseline, with PrivateAssets="all" for build dependencies.
CI MUST run on Windows and Linux.
macOS is not a required 1.0 CI platform.
Tests MUST include valid and invalid forms for:
- static and instance mapper classes;
- non-partial mapper;
- nested mapper with non-partial parent;
- generic mapper or generic containing type;
- abstract mapper;
- unsupported mapper kind;
- invalid mapping signatures;
- async methods;
- unsupported accessibility;
- duplicate configuration;
- invalid
UseMapperregistrations.
Tests MUST include:
- parameterless constructor;
- one parameterized constructor;
- multiple satisfiable constructors;
- constructor ambiguity;
[MappingConstructor];- optional parameters;
- records and record structs;
- record copy-constructor exclusion;
initproperties;requiredmembers;SetsRequiredMembersAttribute;- read-only fields;
- constructor accessibility.
Tests MUST include:
- null/non-null root source;
- enabled and disabled
GuardNonNullSourcebehavior; - nullable/non-null return;
- nullable member to nullable target;
- nullable member to non-null target under
ErrorandThrow; - nullable source paths;
- nullable collections;
- nullable elements;
- nullable-oblivious projects;
- patch null skipping.
Tests MUST cover every mandatory shape and behavior:
- arrays;
- jagged arrays;
- sequence interfaces;
- mutable collection interfaces;
- read-only interfaces;
- lists;
- sets;
- dictionaries;
- nested collections;
- collections of nested objects;
- empty collections;
- null collections;
- nullable elements;
- comparer preservation;
- dictionary key collision;
- single enumeration;
- capacity preallocation when count is cheap;
- mutable-copy behavior;
- unsupported rectangular arrays and custom collections.
Tests MUST include:
- reference target;
- void update;
- destination-returning update;
refstruct target;- null destination;
- writable nested replacement;
- explicit nested mutation;
- collection replacement;
- get-only collection rejection;
IgnoreNullSourceMembersbehavior;- init-only rejection;
- top-level array-update rejection.
Tests MUST include:
- finite recursive tree;
- direct self-cycle;
- indirect cycle;
- cycle through a collection;
- shared non-cyclic reference;
- exception source/destination properties;
- mapping method and member path;
- no tracker allocation for acyclic type graphs;
- one tracker per recursive public call;
- concurrent calls.
Tests MUST include:
- by-name mapping;
- by-value mapping;
- unmatched members;
- unknown runtime numeric values;
- source aliases;
- target aliases;
- zero members;
- flags and composite flags;
- numeric overflow;
- custom converter precedence.
Tests MUST verify the complete precedence chain, including conflicts among:
- explicit
Use; - local
[MappingConverter]; Map{TargetMember};- local mapping method;
- external converter;
- external mapping method;
- default mapping;
- identity and implicit operator;
- enabled explicit operator;
- numeric conversion;
- enum mapping;
- collection mapping;
- structural nested mapping.
Representative generated source MUST compile against every supported consumer TFM and language baseline.
Pull-request CI MUST publish and execute at least one Native AOT sample.
Release CI MUST publish and execute Native AOT samples covering:
- static mapper;
- instance mapper;
- nested collections;
- cycle detection.
A trimmed sample MUST be published and executed. LiteMapper-generated trimming or AOT warnings MUST fail CI.
Automated packaging tests MUST:
- pack all packages;
- create clean consumer projects;
- install packages from a local feed;
- compile and run representative mappings;
- verify one-package normal installation;
- verify generator assets do not become runtime dependencies;
- verify Roslyn assemblies are not copied to consumer output;
- verify
Mammoth.LiteMapper.Abstractionscan be consumed independently; - verify deterministic package content.
CI MUST use Roslyn public API analyzers with:
PublicAPI.Shipped.txt
PublicAPI.Unshipped.txt
It MUST also perform package-level API compatibility validation.
Benchmarks MUST compare pinned versions of:
- manual mapping;
- LiteMapper;
- Mapperly;
- Mapster;
- AutoMapper.
Results are contextual and MUST record runtime, SDK, CPU, OS, library versions, and benchmark configuration.
For straightforward mappings, generated code MUST be structurally equivalent to reasonable handwritten C#.
The specification does not impose a universal nanosecond ratio to manual mapping because tiny benchmark ratios are unstable.
Expected allocations are:
| Feature | Allowed generated overhead |
|---|---|
| Flat new-object mapping | destination object only |
| Nested mapping | destination graph only |
| Collection mapping | destination collection and mapped destination elements only |
| Non-recursive mapping | no tracker allocation |
Recursive ThrowOnCycle |
one tracker state plus required active-path entries |
User converters may allocate independently.
Controlled release benchmarks MUST enforce:
- allocation regressions block release unless explicitly approved and documented;
- statistically significant throughput regressions greater than 10% require review;
- statistically significant throughput regressions greater than 20% block release by default.
Shared pull-request runners MUST NOT use fragile fixed nanosecond thresholds.
LiteMapper guarantees zero runtime reflection for all supported generated features.
All supported generated features MUST be compatible with Native AOT and trimming.
User-provided converter code and instance dependencies remain the consumer's responsibility.
LiteMapper MUST NOT perform:
- telemetry;
- analytics;
- network calls;
- update checks;
- runtime reporting;
- build usage collection.
LiteMapper MUST NOT inject runtime logging or callbacks into generated mapping code.
LiteMapper follows Semantic Versioning.
Compatibility includes:
- public types and members;
- attribute semantics;
- diagnostic IDs and meaning;
- supported mapping signatures;
- configuration precedence;
- observable generated behavior;
- exception types and properties.
Exact generated formatting, local names, and helper organization are not public API and may change between releases.
Diagnostic IDs are stable from the moment they are introduced, including pre-1.0 releases.
Release artifacts MUST use:
- deterministic builds;
- Source Link;
- repository URL and commit metadata;
- symbol packages;
- no machine-specific paths;
- no content-changing timestamps;
- repeatable NuGet packaging.
LiteMapper assemblies MUST NOT be strong-name signed in 1.0.
All source and packages MUST use the MIT license.
Implementation MUST follow this order:
- repository, solution, package, and CI skeleton;
- public abstractions API and public API baselines;
- generator discovery, incremental infrastructure, and declaration diagnostics;
- flat mapping;
- constructors, records,
init,required, and value types; - explicit member configuration and converters;
- nullable behavior;
- nested object mapping;
- arrays, collections, sets, and dictionaries;
- existing-target mapping and patch behavior;
- enum mapping;
- recursive type analysis and
ThrowOnCycle; - capability-based emitted optimization;
- packaging, trimming, and Native AOT validation;
- benchmarks and compiling samples;
- usage documentation assembled from compiling samples.
Every feature milestone MUST begin with failing tests and diagnostic cases.
Implementation-first followed by retrofitted tests is not accepted for required features.
A milestone is complete only when:
- focused tests pass;
- the full test suite passes;
- generated source has been reviewed;
- diagnostics are documented and tested;
- public API baselines are deliberately updated when required;
STATUS.mdandDECISIONS.mdare updated;- no unrelated TODO or disabled test was introduced.
The implementation agent MAY propose a specification change in DECISIONS.md.
It MUST NOT implement semantic changes before explicit approval.
Any unresolved decision or specification ambiguity stops the whole implementation project until resolved.
The agent MUST NOT continue unaffected milestones while a normative ambiguity remains open.
Before implementation stabilizes, this specification is the sole source of truth.
During implementation, sample projects become the authoritative source for usage examples.
The eventual usage guide MUST:
- be assembled from or directly reference real compiling sample source;
- be validated in CI;
- use package references rather than project references in release validation;
- never document an API that does not compile;
- distinguish required 1.0 behavior from deferred features.
A standalone handwritten usage guide MUST NOT be created in advance and treated as a parallel specification.
The following are deferred and MUST NOT appear as partially functional public APIs in 1.0:
- EF Core expression projections;
- expression-tree converter inlining;
- runtime polymorphism;
- reference preservation;
- hooks;
- object factories;
- mapping context propagation;
- open generic mappings;
- generic mapper containers;
- external instance mapper resolution;
- DI registration extensions;
- automatic flattening;
- naming-strategy plugins;
- custom collections;
- immutable collections;
- queues and stacks;
- rectangular arrays;
- async mapping;
- code fixes;
- runtime logging;
- telemetry.
A deferred feature MUST either:
- produce an explicit diagnostic when a generated declaration requests it; or
- remain absent from public API so normal C# rejects the attempted usage.
resolveOption(methodValue, mapperValue, assemblyValue, libraryDefault):
if methodValue != Unspecified:
return methodValue
if mapperValue != Unspecified:
return mapperValue
if assemblyValue exists and assemblyValue != Unspecified:
return assemblyValue
return libraryDefault
For method Boolean overrides:
resolveBoolean(methodState, mapperBoolean):
if methodState == Enabled:
return true
if methodState == Disabled:
return false
return mapperBoolean
matchTargetMember(target, sourceCandidates, policy):
explicit = explicit configuration for target
if explicit exists:
validate and return explicit
candidates = readable public instance properties and fields
if policy == Exact:
return unique ordinal exact name match or none/error
if policy == ExactThenIgnoreCase:
exact = ordinal exact matches
if exact count == 1:
return exact[0]
if exact count > 1:
error
insensitive = ordinal-ignore-case matches
return unique insensitive match or none/error
if policy == IgnoreCase:
insensitive = ordinal-ignore-case matches
return unique insensitive match or none/error
Before name comparison, hidden-member resolution selects the most-derived member, then property over field, and records a warning when hiding occurred.
selectConstructor(targetType, mapping):
eligible = accessible constructors excluding record copy constructors
marked = eligible with MappingConstructor
if marked count > 1:
error
if marked count == 1:
if fully satisfiable(marked[0]):
return marked[0]
error
parameterized = fully satisfiable eligible constructors with parameter count > 0
if parameterized count == 1:
return parameterized[0]
if parameterized count > 1:
maxCount = maximum parameter count
best = parameterized with maxCount
if best count == 1:
return best[0]
error
parameterless = eligible constructors with zero parameters
if parameterless count == 1:
return parameterless[0]
if parameterless count > 1:
error
error
resolveValue(sourceValue, targetType, targetMember):
try explicit Use
select explicit source path when configured
if selected source value is directly assignable, use it
otherwise continue with that selected source value
try local MappingConverter
try local Map{TargetMember}
try explicit local mapping method
try registered external converter
try registered external mapping method
try default mapping
try identity or implicit C# conversion
try enabled explicit operator / numeric policy
try enum strategy
try collection mapping
try generated private structural nested mapping
otherwise error
At every stage, equal-precedence ambiguity is an error.
mapCollection(source, targetShape):
if source is null:
apply effective null collection strategy
determine destination concrete type
determine count only through cheap count/index capability
allocate destination with capacity when available
enumerate source exactly once:
map key and value independently for dictionary
otherwise map one element to one element
add using normal destination semantics
return destination
build directed graph:
node = closed source/destination mapping pair
edge A -> B = A requires nested or element mapping B
compute strongly connected components
component is recursive when:
component contains multiple nodes
OR one node has a self-edge
only recursive components receive cycle-tracker plumbing
mapTracked(source, tracker, memberPath):
if source is null:
apply null behavior
if tracker.activeReferences already contains source by reference identity:
throw LiteMapperCycleException
add source to tracker.activeReferences
try:
perform mapping and recursive calls
finally:
remove source from tracker.activeReferences
LiteMapper 1.0 is complete only when all of the following are true:
- every required feature in this specification is implemented;
- every required diagnostic exists and has focused tests;
- all supported consumer TFMs compile and pass runtime tests;
- all required C# language-version tests pass;
- minimum, intermediate, and current Roslyn versions pass;
- generator declaration, runtime, snapshot, incremental, packaging, trimming, and Native AOT tests pass;
- Windows and Linux CI pass;
- public API compatibility checks pass;
- allocation and benchmark acceptance criteria are met;
- NuGet packages are reproducible;
- clean consumer projects install from a local feed and run successfully;
- Roslyn and generator assemblies do not become runtime dependencies;
- no supported feature uses reflection, dynamic dispatch, runtime scanning, or runtime code generation;
- every deferred feature is absent or explicitly diagnosed;
- samples compile from clean package references;
- the usage guide is assembled from compiling samples;
- no unresolved P0 or P1 issue exists;
- no unresolved normative ambiguity exists;
STATUS.mddeclares every milestone complete with evidence.
Compilation alone is not completion.
This file supersedes every earlier MinimalMapper or LiteMapper draft, audit bundle, README, usage guide, ZIP archive, chat example, or provisional design note.
During implementation:
- approved updates MUST modify this file first;
DECISIONS.mdMUST record the approved reason;- tests and samples MUST be updated in the same change;
- generated documentation MUST follow the implementation and compiling samples.
No other document may silently redefine LiteMapper behavior.