MorphMapping 1.1.2
dotnet add package MorphMapping --version 1.1.2
NuGet\Install-Package MorphMapping -Version 1.1.2
<PackageReference Include="MorphMapping" Version="1.1.2" />
<PackageVersion Include="MorphMapping" Version="1.1.2" />
<PackageReference Include="MorphMapping" />
paket add MorphMapping --version 1.1.2
#r "nuget: MorphMapping, 1.1.2"
#:package MorphMapping@1.1.2
#addin nuget:?package=MorphMapping&version=1.1.2
#tool nuget:?package=MorphMapping&version=1.1.2
MorphMapping
A lightweight, reflection-based object mapper for .NET with customizable converters, per-pair hooks and a DI-friendly configuration model.
The library itself has no dependency on Microsoft.Extensions.DependencyInjection — it ships as two packages:
MorphMapping— the core mapper. Standalone, depends only onMicrosoft.Extensions.Logging.Abstractions.MorphMapping.DependencyInjection— integration withMicrosoft.Extensions.DependencyInjection.
Features
- Property-name mapping (case-insensitive) with support for nested objects, collections, arrays and dictionaries.
- Standalone configuration via
MapperBuilder, or through DI (services.AddMorphMapper(...)). - Property renaming and ignoring (including the
[IgnoreMapping]attribute). - Custom value providers for specific destination properties.
Before/Afterhooks at both per-pair and global levels.- Custom converters: global (
MapperOptions.Converters), per-property and per-class via[MappingConverter(typeof(MyConverter))]. - Constructor-based mapping by parameter name.
- Built-in support for
Nullable<T>, enums (string↔enum, int↔enum), numeric conversions,IDictionary,IEnumerable. MappingContextto pass arbitrary state through the mapping pipeline.
Build and test
dotnet restore MorphMapping.sln
dotnet build MorphMapping.sln -c Release
dotnet test MorphMapping.sln -c Release
Usage
Standalone (no DI)
using MorphMapping;
var mapper = new MapperBuilder()
.ConfigureOptions(opts => opts.ThrowOnError = true)
.Configure<SourcePerson, DestPerson>(cfg => cfg
.MapProperty(nameof(SourcePerson.Name), nameof(DestPerson.FullName))
.IgnoreProperty(nameof(DestPerson.SecretCode))
.AfterMapping((src, dst) => dst.FullName = dst.FullName?.Trim()))
.Build();
var dest = mapper.Map<DestPerson>(new SourcePerson { Name = "Alice", Age = 30 });
With Microsoft.Extensions.DependencyInjection
Install MorphMapping.DependencyInjection and register the mapper as a singleton.
AddMorphMapper takes an optional Action<MapperOptions> for options-level
configuration and returns the MapperBuilder so per-pair configuration can
be chained fluently:
using Microsoft.Extensions.DependencyInjection;
using MorphMapping;
using MorphMapping.DependencyInjection;
var services = new ServiceCollection();
services.AddLogging();
services
.AddMorphMapper(opts =>
{
opts.ThrowOnError = true;
opts.Converters.Add(new MoneyToDtoConverter());
})
.Configure<SourcePerson, DestPerson>(cfg => cfg
.MapProperty(nameof(SourcePerson.Name), nameof(DestPerson.FullName))
.IgnoreProperty(nameof(DestPerson.SecretCode)));
var provider = services.BuildServiceProvider();
var mapper = provider.GetRequiredService<IMapper>();
The options-level lambda runs first (before any builder actions) and always
sees a fresh MapperOptions. Any .Configure<...>() chained after
AddMorphMapper is picked up lazily when the mapper is materialized from the
container, so call order is forgiving.
Mapping into an existing instance
var existing = new DestPerson { FullName = "preserved" };
mapper.Map(source, existing);
Passing context
var dest = mapper.Map<DestPerson>(source, ctx => ctx.Add("culture", "ru-RU"));
Advanced scenarios
Custom value provider for a destination property
builder.Configure<SourcePerson, DestPerson>(cfg => cfg
.MapProperty(nameof(DestPerson.FullName), src => $"{src.Name} ({src.Age})"));
Before / After hooks
builder.Configure<SourcePerson, DestPerson>(cfg => cfg
.BeforeMapping((src, dst) => { /* ... */ })
.AfterMapping((src, dst, ctx) => { /* ... */ }));
Full custom mapping logic
builder.Configure<SourcePerson, DestPerson>(cfg => cfg
.CustomMapping((src, dst) =>
{
dst.Name = src.Name.ToUpperInvariant();
dst.Age = src.Age;
}));
Global hooks
builder
.GlobalBeforeMapping((s, d) => { /* audit */ })
.GlobalAfterMapping((s, d) => { /* log */ });
Global converter for a type
public sealed class MoneyToDtoConverter : MappingConverter<Money, MoneyDto>
{
public override MoneyDto? Convert(Money? source, MoneyDto? destination, MappingContext context)
{
if (source is null) return null;
var dto = destination ?? new MoneyDto();
dto.Formatted = $"{source.Amount:F2} {source.Currency}";
return dto;
}
}
services.AddMorphMapper(opts => opts.Converters.Add(new MoneyToDtoConverter()));
Per-property converter via attribute
public sealed class UpperCaseConverter : MappingConverter<string, string>
{
public override string? Convert(string? source, string? destination, MappingContext context)
=> source?.ToUpperInvariant();
}
public class DestDto
{
[MappingConverter(typeof(UpperCaseConverter))]
public string Title { get; set; } = string.Empty;
}
Per-class converter via attribute
[MappingConverter] can also be placed on a class. When the pipeline targets
that class as a destination (root-level, nested property, collection item),
the attached converter is used instead of the default object-copy flow — no
global registration required. The attribute can equally be placed on a source
class to redirect everything leaving that type through a converter.
Precedence, from most- to least-specific:
- per-property
[MappingConverter]on the destination property - per-class
[MappingConverter]on the destination type - per-class
[MappingConverter]on the source type - global converters from
MapperOptions.Converters - default contract (
ObjectContract,EnumContract, etc.)
public sealed class MoneyToDtoConverter : MappingConverter<Money, MoneyDto>
{
public override MoneyDto? Convert(Money? source, MoneyDto? destination, MappingContext context)
{
if (source is null) return null;
var dto = destination ?? new MoneyDto();
dto.Formatted = $"{source.Amount:F2} {source.Currency}";
return dto;
}
}
// Anything mapped *into* MoneyDto (root or nested) uses MoneyToDtoConverter.
[MappingConverter(typeof(MoneyToDtoConverter))]
public class MoneyDto
{
public string Formatted { get; set; } = string.Empty;
}
The converter's declared SourceType / DestinationType still have to be
compatible with the actual runtime types — mismatched attributes are safely
ignored and the pipeline falls through to the next rule.
Ignoring a property via attribute
public class Dto
{
public string Public { get; set; } = string.Empty;
[IgnoreMapping]
public string Secret { get; set; } = string.Empty;
}
Mapper options (MapperOptions)
| Option | Default | Description |
|---|---|---|
ThrowOnError |
false |
Re-throws exceptions from user actions, wrapped in MappingException where appropriate. |
LogErrors |
true |
Logs mapping errors through ILogger<Mapper> (if a logger factory was provided). |
FallbackToParameterlessConstructor |
true |
Falls back to the parameterless constructor when no parametric one matches. |
ContractResolver |
DefaultContractResolver |
Resolver used to discover type contracts. |
Converters |
empty | List of global MappingConverter instances. |
Built-in type support
- Primitives and
string— viaConvert.ChangeTypeandEnum.Parse. Enum— from string (Enum.Parse) and from an integer value (Enum.ToObject).Nullable<T>— unwrapped transparently to the underlying type.- Arrays — sized from the source collection.
IEnumerable<T>,ICollection<T>,List<T>— populated viaAdd.IDictionary<TKey, TValue>— iterated asKey/Valuepairs and added viaAdd(key, value).- Arbitrary classes/structs — via
ObjectContract: reflective property walk with support for constructor-by-name.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
- Microsoft.Extensions.Logging.Abstractions (>= 6.0.4)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on MorphMapping:
| Package | Downloads |
|---|---|
|
MorphMapping.DependencyInjection
Microsoft.Extensions.DependencyInjection integration for MorphMapping. |
GitHub repositories
This package is not used by any popular GitHub repositories.