Mapping-Object
1.3.0
See the version list below for details.
dotnet add package Mapping-Object --version 1.3.0
NuGet\Install-Package Mapping-Object -Version 1.3.0
<PackageReference Include="Mapping-Object" Version="1.3.0" />
paket add Mapping-Object --version 1.3.0
#r "nuget: Mapping-Object, 1.3.0"
// Install Mapping-Object as a Cake Addin #addin nuget:?package=Mapping-Object&version=1.3.0 // Install Mapping-Object as a Cake Tool #tool nuget:?package=Mapping-Object&version=1.3.0
Mapping Object
In several cases object mappings are useful. When a mapping needs to be done in several places, it'd be nice to write mapping code only once - or not even at all. This is where the Mapping Object library (sustainably grown) may help you.
I know about the famous (and well done) AutoMapper library already, which could do everything you could do with my Mapping Object library, too - and even more. But in some szenarios AutoMapper may be just too much, while the Mapping Object library offers a simple and slender tool character, which may be more than enough in many cases already, I'd say.
While I use the Mapping Object library in my own projects, and it fits all of my requirements, I'd prefer to switch to AutoMapper in case it would make sense, rather than adding new features to the Mapping Object library. However, this doesn't mean that this version of the library is the final version already! But it means that the current feature set may not be extended so much in the future.
How to get it
The libraries are available as NuGet packages:
Usage
NOTE: The main properties need a public getter/setter, while the source object properties need only a public getter (if you don't use reverse mapping, too).
General
Often a mapping is being done in one way only - in this case there's a main object and a source object. The source objects properties will be mapped to the main object properties.
In a bi-directional mapping it's also required to map the main object properties back to the source object properties (reverse mapping).
In the best case, you don't need any mapping configuration, and all the work can be done using a single line of code:
using wan24.MappingObject;
// Map property values of sourceType to mainType properties
Mappings.MapFrom(sourceType, mainType);
Or the reverse mapping:
// Map mainType property values back to sourceType properties
Mappings.MapTo(mainType, sourceType);
The types of these examples may be:
public class MainType
{
public bool MappedProperty { get; set; }
[SkipMapping]
public int NotMappedProperty { get; set; } = 1;
}
public class SourceType
{
public bool MappedProperty { get; set; } = true;
public int NotMappedProperty { get; set; }
public int NotMappedProperty2 { get; set; }
}
NOTE: The mapping configuration needs to be done in the main type, only. Any mapping configuration in the source type doesn't have any effect.
You want to map SourceType
to MainType
, so you may register the mapping
(which is optional and can be done automatic, if no customizations are being
used):
Mappings.Add(typeof(SourceType), typeof(MainType));
NOTE: A type can also be an interface, an abstract type, or a generic type
definition. The Mappings.Find
method fill find the best matching mapping for
a type pair, where the main object type has the first priority.
This will create a mapping for all public instance properties of MainType
which have a public getter/setter and do not have the attribute SkipMapping
.
Properties that don't exist in the source type will be skipped.
This mapping can now be applied:
MainType main = new();
Mappings.MapFrom(new SourceType(), main);
Assert.IsTrue(main.MappedProperty);
Assert.AreEqual(1, main.NotMappedProperty);
In this case a reverse mapping is possible already, too:
SourceType source = new();
Mappings.MapTo(new MainType(), source);
Assert.IsFalse(source.MappedProperty);
Assert.AreEqual(0, source.NotMappedProperty);
NOTE: In general Map*From
maps a source type to the main type, while
Map*To
maps a main type to a source type (reverse mapping).
Fluent API
Mappings.Add(typeof(SourceType), typeof(MainType))
.ExcludeProperties(nameof(SourceType.ExcludedProperty1), nameof(SourceType.ExcludedProperty2), ...)
.ConfigureMapping(nameof(SourceType.Property3), (mappings, mapping) =>
{
// Mapping detail configuration
mapping.WithSourceConverter(value => ...)
// ...and the reverse mapping
.WithMainConverter(value => ...);
})
.WithMapping(
(source, main) =>
{
// Fully customized source to main object mapping logic
},
(main, source) =>
{
// Fully customized main to source object mapping logic (reverse mapping)
}
);
There are fluent extensions for the MappingConfiguration
and
(Async)Mapping
types which allow you to do all configuration using a fluent
API.
Cloning an object
In case you want to clone an object, which isn't cloneable by default (doesn't
implement ICloneable
or any cloning method):
AnyType clonedInstance = Mappings.MapFrom(instance, new AnyType());
Of course you can customize the cloning by using custom getter delegates, which will clone property values, too.
Use a different source property
The automated mapping uses the same property names for the main and the
source types. In case you want to use different property names, you can use
the wan24.MappingObject.MapAttribute
attribute in the main type to specify
another source object property name:
public class MainType
{
// Use "nameof" to be safe for future modifications here
[Map(nameof(SourceType.MappedProperty2))]
public bool MappedProperty { get; set; }
}
public class SourceType
{
public bool MappedProperty2 { get; set; } = true;
}
Mappings.Add(typeof(SourceType), typeof(MainType));// Optional - can still be done automatic
MainType main = new();
Mappings.MapFrom(new SourceType(), main);
Assert.IsTrue(main.MappedProperty);
This also affects the reverse mapping.
You could also skip the Map
attribute and define the source type property
name during the mapping registration:
Mappings.Add(
typeof(SourceType),
typeof(MainType),
new Mapping(nameof(MainType.MappedProperty), nameof(SourceType.MappedProperty2))
);
NOTE: If a main type property wasn't found in the source type (during the automated mapping registration), it won't be mapped automatic.
Convert the mapped value
You can define value converters in case the source value type needs to be converted before setting it in the main type property:
public class MainType
{
public bool MappedProperty { get; set; }
}
public class SourceType
{
public string MappedProperty { get; set; } = true.ToString();
}
Mappings.Add(
typeof(SourceType),
typeof(MainType),
new Mapping(
nameof(MainType.MappedProperty),
// Converts the source value for the main type property
(v) => bool.Parse((string)v!),
// Converts the main value for the source type property (for reverse mapping)
(v) => v!.ToString()
)
);
MainType main = new();
Mappings.MapFrom(new SourceType(), main);
Assert.IsTrue(main.MappedProperty);
Full mapping customization
To create a fully customized mapping using getter delegates:
public class MainType
{
public string Name { get; set; } = null!;
}
public class SourceType
{
public string FirstName { get; set; } = "John";
public string LastName { get; set; } = "Doe";
}
Mappings.Add(
typeof(SourceType),
typeof(MainType),
new Mapping(
nameof(MainType.Name),
nameof(SourceType.FirstName),
// Converts the source value for the main type property
sourceGetter: (source, main) =>
{
SourceType sourceType = (SourceType)source;
return $"{sourceType.FirstName} {sourceType.LastName}";
},
// Converts the main value for the source type property (for reverse mapping)
mainGetter: (main, source) =>
{
MainType mainType = (MainType)main;
SourceType sourceType = (SourceType)source;
string[] name = mainType.Name.Split(' ');
sourceType.LastName = name[1];
return name[0];// Will be set to "sourceType.FirstName", 'cause it's this mappings target property
}
)
);
// "sourceGetter" will be applied:
MainType main = new();
Mappings.MapFrom(new SourceType(), main);
Assert.AreEqual("John Doe", main.Name);
// "mainGetter" will be applied:
main.Name = "John Smith";
SourceType source = new();
Mappings.MapTo(main, source);
Assert.AreEqual("John", source.FirstName);
Assert.AreEqual("Smith", source.LastName);
Or using mapping delegates:
Mappings.Add(
typeof(SourceType),
typeof(MainType),
new Mapping(
// ID of the custom mapping
nameof(MainType.Name),
// Map source -> main
(source, main) =>
{
SourceType sourceType = (SourceType)source;
MainType mainType = (MainType)main;
main.Name = $"{sourceType.FirstName} {sourceType.LastName}";
},
// Map main -> source (reverse mapping)
(main, source) =>
{
MainType mainType = (MainType)main;
SourceType sourceType = (SourceType)source;
string[] name = mainType.Name.Split(' ');
sourceType.FirstName = name[0];
sourceType.LastName = name[1];
}
)
);
The difference between the two Mappings.Add
calls is: The first call uses
getter delegates, while the second call uses mapper delegates. A getter
delegate returns the value to map, a mapper delegate does the whole mapping
for one or multiple values (and returns nothing).
When using mapper delegates, the ID has to be unique for one mapping, and it doesn't need to match a property name (but if it does, it'd overwrite a generated property mapping). You could perform the whole object mapping within the mapping delegates or extend the automatic mappings with customizations. In this example nothing can be mapped automatic, and only the mapping delegates will do the work.
The MappingObjectBase
base type
Using the MappingObjectBase
type allows to customize the mapping by
overriding the Map*
methods in your implementing type:
public class MainType : MappingObjectBase<SourceType>
{
public MainType() : base() { }
public MainType(SourceType source) : base(source) { }
// Mapped properties may be here
public override void MapFrom(SourceType source)
{
base.MapFrom(source);// Optional
// Your custom source to main object mapping logic
}
public override void MapTo(SourceType source)
{
base.MapTo(source);// Optional
// Your custom main to source object mapping logic
}
}
You can configure the MainType
properties as usual (using attributes) and
implement custom mapping logic as required. The mappings will be created
automatic for this type as for any other type which is being used as the main
type for a mapping. To avoid that, you can register a custom mapping in the
static constructor of your implementing type, for example.
The same functionality including casting you'll get using the
MappingObjectCastableBase
base class. Then you could cast like this:
public sealed class MainType : MappingObjectCastableBase<SourceType, MainType>
{
...
}
MainType main = (MainType)source;// One explicit cast is still required :(
The IMappingObject
interface
The IMappingObject
comes as generic and as non-generic type. Usually you
want to use the generic interface, but you could also implement only the non-
generic interface, also.
For the implementation and the usage please have a look at the TestType4.cs
code in the tests project. You can use the interface for objects that can't
extend the MappingObjectBase
type.
The MappingObjectAdapter
In case you don't want to extend MappingObjectBase
, and you don't want to
implement IMappingObject
, you can use a MappingObjectAdapter
:
public sealed class YourTargetType : IAdapterMappingObject<YourSourceType, YourTargetType>
{
private readonly MappingObjectAdapter<YourSourceType, YourTargetType> MappingObjectAdapter;
public YourTargetType() => MappingObjectAdapter = new(this);
public void MapFrom(YourSourceType source) => MappingObjectAdapter.MapFrom(source);
public void MapTo(YourSourceType source) => MappingObjectAdapter.MapTo(source);
}
The YourTargetType
type now has the same mapping capabilities as a type
which extends MappingObjectBase
.
For an asynchronous object mapping adapter use AdapterMappingObjectBase
and
IAdapterMappingObjectAsync
.
Enumerable mapping extensions
You can use the MapAllFrom
and MapAllTo
enumerable extension methods for
mapping a list of main and/or source objects:
// Convert source type objects to main type objects
MainType[] mainObjects = sourceObjects.MapAllFrom<SourceType, MainType>().ToArray();
// Convert main type objects to source type objects
SourceType[] sourceObjects = mainObjects.MapAllTo<MainType, SourceType>().ToArray();
The methods which accept a list of key/value pairs require the key to be the source object, while the value requires to be the target object for the mapping.
If required, you can use a factory method for creating the mapping target type object instances, too - otherwise instances will be created using the empty constructor of the target type.
Mapping registration
I'd suggest to perform a mapping registration in the static constructor of the
main type, which would be a kind of lazy loading. You don't have to register
a mapping at all, if you don't use custom Mapping
instances and all mapping
configuration can be done in the main type (or isn't required at all).
If you use the Mappings.Add
method which consumes a configuration, no
automatic mapping will be created. To create the automatic mappings, use the
Mappings.Create
method.
Asynchronous object mapping
Using the Mapping Object Async library the whole thing will be extended by
asynchronous mapping delegates and methods which allow fully asynchronous
work with continuous cancellation token support and asynchronous enumerables,
too - an example using the MappingObjectAsyncBase
base type:
public class MainType : MappingObjectAsyncBase<SourceType>
{
...
public override async Task MapFromAsync(
SourceType source,
CancellationToken cancellationToken = default
)
{
await base.MapFromAsync(source, cancellationToken);// Optional
// Apply your custom asynchronous mappings here
}
public override async Task MapToAsync(
SourceType source,
CancellationToken cancellationToken = default
)
{
await base.MapToAsync(source, cancellationToken);// Optional
// Apply your custom asynchronous mappings here
}
}
public class SourceType
{
...
}
MainType main = new();
SourceType source = new();
// Using "*Async" here - the synchronous methods will refer to the
// asynchronous methods in the base type, which may optional be overridden,
// too
await main.MapFromAsync(source);
await main.MapToAsync(source);
// Or using the static mapping methods:
await AsyncMappings.MapFromAsync(source, main);
await AsyncMappings.MapToAsync(main, source);
In case you need to use customized Mapping
instances, use AsyncMapping
instead (supported by Mappings.Add*
, too). However, mixing synchronous
Mapping
and AsyncMapping
works, when calling a AsyncMapings.Map*
method. The Mapping
constructors are not supported in the AsyncMapping
type - instead use the similar constructors which use asynchronous delegates.
NOTE: Don't use the Mappings.Map*
methods with types that are using
asynchronous mappings. If the synchronous mapping methods are being used, a
NotSupportedException
would be thrown!
To adapt to asynchronous mapping, simply replace the used types:
Mappings
→AsyncMappings
(where possible!)Mapping
→AsyncMapping
(where required)MappingObjectBase
→MappingObjectAsyncBase
IMappingObject
→IMappingObjectAsync
The asynchronous enumerable mapping extensions have support for IEnumerable
and IAsyncEnumerable
. Also an asynchronous object factory method may be
used, if applicable.
Mapping to init-only (required
) properties without a main property
// A type with a required (init-only) property
public class YourType
{
public required string StringProperty { get; set; }
}
// Mapping configuration
Mappings.Add(typeof(AnyType), nameof(YourType))
.WithMapping(
"YourTypeCustomMapping",
(source, main) => { },
(main, source) => source.StringProperty = "Value"
);
// Mapping (which will use the previously created mapping configuration)
YourType instance = Mappings.MapTo(anyTypeInstance, (YourType)Activator.CreateInstance(typeof(YourType)));
Actually the trick is to use Activator.CreateInstance
for YourType
, 'cause
this method doesn't require to set the required property values 😃 Then later
the mapping comes in effect to set a property value for the required
StringProperty
.
NOTE: This is only required, if AnyType
doesn't have a StringProperty
,
which would be mapped automatic already.
Execute handler after/before mapping
When using the Mappings.Add
method for a mapping registration, you'll get a
MappingConfig
as return value. In this mapping configuration you can set
handler delegates for
- handling objects before mapping
- handling objects after mapping
- handling objects before reverse mapping
- handling objects after reverse mapping
Those handlers will be executed when
- using any
Mappings.Map*
method - (reverse) mapping a
MappingObjectBase
but not when calling a Map*
method of an IMappingObject
directly. Within a
method in an IMappingObject
you can decide to load and execute possible
handlers by evaluating the applyDefaultMappings
parameter: If the value is
false
, handlers will be executed from the Mappings.Map*
methods. Otherwise
you may process as the MappingObjectBase.Map*
do.
Nested object mapping
In case
- the value isn't
null
, not a value type and can't be set to the target property and - the target property type isn't a value type and can be instanced (f.e. using a factory method (required for interface types)) and
it's possible to map deep objects - in the best case without any manual mapping configuration.
Example:
public class A
{
public C Value { get; set; } = new();
}
public class B
{
public D Value { get; set; } = new();
}
public class C
{
...
}
public class D
{
...
}
For mapping A
↔ B
, the value of Value
needs to be converted. If it's
possible to map C
↔ D
(automatic or using a manual mapping
configuration), the A
↔ B
mapping can be done automatic: Because the
mapper can see that C
can't be assigned to D
, it tries automatic type
conversion.
For the type conversion, the mapper will instance the required target value
type, if the target property has no value yet (optional using a factory method
(see SourceInstanceFactory
and MainInstanceFactory
properties of a
Mapping
)).
WARNING: It's possible to nest synchronous mapped objects into an asynchronous mapped object, but not the opposite (the parent object needs to be mapped asynchronous when mixing mapping methods!).
Missing a validation (feature)?
If you're missing a validation or a feature, please open an issue.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. 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. |
-
net6.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Mapping-Object:
Package | Downloads |
---|---|
Mapping-Object-Async
Asynchronous extensions for the Mapping-Object |
GitHub repositories
This package is not used by any popular GitHub repositories.