Convention-based object-to-object compile time mapper for .NET.
It simplifies the process of transferring data between objects, reducing the need for manual mapping code. dotMap leverages a set of conventions to map properties between source and destination types, minimizing configuration while maintaining flexibility. This library is particularly useful in scenarios where objects have similar structures, such as in data transfer objects (DTOs) and domain models, enabling developers to focus more on business logic rather than boilerplate mapping code.
dotMap is ideal for developers looking for a streamlined and reliable solution for object mapping in .NET applications, improving code maintainability and reducing the likelihood of errors during data transfer operations.
- Key features
- Installation
- Usage example
- How does it work?
- Tutorials
- Methods
- Attributes
- Interfaces
- Roadmap
- License
- Contribution
- Contact
- Convention-Based Mapping: Automatically maps properties with matching names between source and destination objects.
- Compile-Time Checking: Ensures mappings are verified at compile time, reducing runtime errors.
- Nested Mappings: Supports deep object graphs, allowing complex objects to be mapped effortlessly.
- Configurable: Easily configurable to support value transformations, and meet object construction requirements.
PM> Install-Package dotMap
- Alternatively, add dotMap from the .NET CLI.
dotnet add package dotMap
- Define map configuration
- Get instance of the class by calling automatically generated extension method that is unique for your object
using FluentAssertions;
using Xunit;
public class Example
{
[Fact]
public void Test()
{
var source = new Source { Name = "Foo" };
var destination = source.MapToDestination();
// destination.Name has the same value as source.Name
destination.Name.Should().Be(source.Name);
}
private class Configuration : IMapConfig
{
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
.To<Destination>();
}
}
}
internal class Destination
{
public string Name { get; set; }
}
public class Source
{
public string Name { get; set; }
}
π Check more examples right in the source code.
Once the map configuration is defined, dotMap
generates new files with classes that provide extension methods
for the source
object that allow you to get an instance of the target
object while preserving the source
values
based on the defined configuration.
classDiagram
class Source {
+string name
+getName()
}
class Destination {
+string Name
+string GetName
}
class Configuration {
+ConfigureMap(MapConfig config)
}
class IMapConfig {
<<Interface>>
+ConfigureMap(MapConfig config)
}
class ConfigurationExtensions {
<<Autogenerated>>
Destination MapToDestination(this Source source)
IEnumerable<Destination> MapToDestination(this IEnumerable<Source> source)
}
note for ConfigurationExtensions "var source = new Source { name = "Foo" };
// Get mapped object by calling extension method
var destination = source.MapToDestination();
asd"
Configuration<|--IMapConfig
ConfigurationExtensions-->Configuration
ConfigurationExtensions..>Source
ConfigurationExtensions..>Destination
Here is the code that is generated for the previous example.
namespace dotMap.Tests.Examples
{
public static class Tests_ConfigurationExtensions
{
internal static dotMap.Tests.Examples.Destination MapToDestination(this dotMap.Tests.Examples.Source source)
{
var result = new dotMap.Tests.Examples.Destination()
{
Name = source.Name
};
return result;
}
internal static IEnumerable<dotMap.Tests.Examples.Destination> MapToDestination(this IEnumerable<dotMap.Tests.Examples.Source> source)
{
return source?.Select(item => item == null ? null : Tests_ConfigurationExtensions.MapToDestination(item));
}
}
}
There are several mechanisms for defining a map configuration.
One is to define a configuration in a separate class by implementing the ConfigureMap
method, as follows.
private class Configuration : IMapConfig
{
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
.To<Destination>()
.WithDestinationNamingConvention<PascalCaseConvention>();
}
}
An alternative solution is to define the configuration directly in the object definition.
internal class Destination : IMappable<Destination>
{
public string Name { get; set; }
public void ConfigureMap(MapConfig<Destination> map)
{
map
.From<Source>()
.WithDestinationNamingConvention<PascalCaseConvention>();
}
}
The other way to define map configuration is to use attributes.
[MapFrom(typeof(Source), DestinationMembersNamingConvention = typeof(PascalCaseConvention))]
internal class Destination
{
public string? Name { get; set; }
[Ignore]
public int Year { get; set; }
}
public class Source
{
public string? name { get; set; }
public int year { get; set; }
}
It is allowed to have multiple configurations.
Also, a single source
object can be mapped to multiple destination
objects.
private class Configuration : IMapConfig
{
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
.To<Destination>();
config
.Map<Source>()
.To<Destination2>()
.Ignore(m => m.Year);
}
}
Here is another example where attributes are used for the same purpose.
[MapFrom(typeof(Source))]
[MapFrom(typeof(Source2))]
internal class Destination
{
public string? Name { get; set; }
// Ignore for all mappings
[Ignore]
public int Year { get; set; }
// Ignore for `Source2` mapping only
[Ignore(For = typeof(Source2))]
public decimal Cost { get; set; }
}
If member names of the source
and destination
objects match, the map configuration can be as simple as following.
private class Configuration : IMapConfig
{
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
.To<Destination>();
}
}
However, if the source
object uses a different naming convention than the destination
object, we need to specify
the naming conversion rules to match object members between the objects.
private class Configuration : IMapConfig
{
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
.To<Destination>()
.WithDestinationNamingConvention<PascalCaseConvention>();
}
}
internal class Destination
{
public string? Name { get; set; }
}
public class Source
{
public string? name { get; set; }
}
Another example is constructor parameters, which may differ between source
and destination
objects,
so the naming conversion rule should be specified to match constructor parameters.
[MapFrom(typeof(source),
DestMappingMode = DestMappingMode.MapToConstructor,
DestinationConstructorNamingConvention = typeof(CamelCaseConvention))]
internal class Destination
{
public Destination(string firstName)
{
FirstName = firstName;
}
public string FirstName { get; }
}
public class source
{
public string first_name { get; set; }
}
As long as source
and destination
objects use the same naming convention, it can be any.
However, if object member names need to be converted to a different naming convention to match,
the naming conversion should be used.
The following naming conversions are built in and supported:
- CamelCase
- pascalCase
- snake_case
You can choose which object members to map, such as properties, fields, and methods. By default, properties and fields are mapped.
This behavior can be configured by specifying one of the MappingMode
:
MapPropsAndFields
to map properties and fields onlyMapMethods
to map methods onlyMapAllMembers
to map properties, fields, and methods
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
.To<Destination>()
.WithMappingMode(SourceMappingMode.MapAllMembers);
}
Consider the following example where SourceMappingMode
is set to map methods only.
Since Source.GetCost()
is a method, and thus can be mapped, and has a matching member name of
which is a property of Destination.GetCost
.
private class Configuration : IMapConfig
{
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
.To<Destination>()
// Source.GetCost() gets mapped to Destination.GetCost
.WithMappingMode(SourceMappingMode.MapMethods);
}
}
internal class Destination
{
public int GetCost { get; set; }
}
public class Source
{
public int GetCost() => 100;
}
If a particular member is not needed, it can be excluded from the map configuration so that its value is not copied.
using FluentAssertions;
using Xunit;
private class Configuration : IMapConfig
{
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
// Name property gets value copied
.To<Destination>()
// Year property is ignored and will equal default value of 0
.Ignore(m => m.Year);
}
}
internal class Destination
{
public string? Name { get; set; }
public int Year { get; set; }
}
public class Source
{
public string? Name { get; set; }
public int Year { get; set; }
}
If the object construction needs to pass an argument, use ConstructFrom
.
This could also be used to adjust the value of the member. However, depending on the design requirements of an object, there are other approaches available. Read more in the Customize member value tutorial.
using FluentAssertions;
using Xunit;
public class Example
{
[Fact]
public void Test()
{
var source = new Source { Name = "Foo", Year = 2024 };
var destination = source.MapToDestination();
destination.Name.Should().Be("Foo 2024");
}
private class Configuration : IMapConfig
{
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
.To<Destination>()
.ConstructFrom(source => new Destination($"{source.Name} {source.Year}"))
.Ignore(m => m.Name);
}
}
}
internal class Destination(string name)
{
public string Name { get; init; } = name;
}
public class Source
{
public string Name { get; set; }
public int Year { get; set; }
}
Pass an action to the Finally
method call to execute the logic required to adjust destination object.
using FluentAssertions;
using Xunit;
public class Example
{
[Fact]
public void Test()
{
var source = new Source { Name = "Foo", Year = 2024 };
var destination = source.MapToDestination();
destination.Name.Should().Be("Foo 2024");
}
private class Configuration : IMapConfig
{
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
.To<Destination>()
.Finally((source, destination) => destination.Name = $"{source.Name} {source.Year}");
}
}
}
internal class Destination
{
public string Name { get; set; }
}
public class Source
{
public string Name { get; set; }
public int Year { get; set; }
}
When a destination object change involves multiple member changes, it can be split to improve configuration readability and simplify maintenance.
using FluentAssertions;
using Xunit;
public class Example
{
[Fact]
public void Test()
{
var source = new Source { Name = "Foo", Year = 2024, Cost = 100 };
var destination = source.MapToDestination();
destination.Name.Should().Be("Bar");
destination.Year.Should().Be(2023);
destination.Cost.Should().Be(0);
}
internal class Configuration : IMapConfig
{
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
.To<Destination>()
.ForMember(d => d.Name, s => "Bar")
.ForMember(d => d.Year, s => s.Year - 1)
.ForMember(d => d.Cost, TransformCost);
}
// Note the class visibility access modifier, so `TransformCost` can be called
public static decimal TransformCost(Source s) => 0;
}
}
internal class Destination
{
public string Name { get; set; }
public int Year { get; set; }
public decimal Cost { get; set; }
}
public class Source
{
public string Name { get; set; }
public int Year { get; set; }
public decimal Cost { get; set; }
}
While it's possible to have value transformation logic defined in the map configuration, it can be extended by
accepting additional parameters. To implement this, WithParameter
should be defined, listing the arguments to be
passed to the map configuration, as follows.
using FluentAssertions;
using Xunit;
public class Example
{
[Fact]
public void Test()
{
var source = new Source { Name = "Foo" };
var destination = source.MapToDestination("Bar");
destination.Name.Should().Be("Bar");
}
private class Configuration : IMapConfig
{
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
.To<Destination>()
.WithParameter<string>()
.ConstructFrom((source, prm) => new Destination { Name = prm })
.Ignore(m => m.Name);
}
}
}
internal class Destination
{
public string Name { get; set; }
}
public class Source
{
public string Name { get; set; }
}
In case of multiple parameters, the map configuration may look like the following.
private class Configuration : IMapConfig
{
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
.To<Destination>()
.WithParameter<(string name, int year)>()
.ConstructFrom((source, args) => new Destination { Name = $"{args.name} {args.year}" })
.Ignore(m => m.Name);
}
}
When a list of objects is being passed to the MapToDestination
call, each item in the list is mapped according to the
map configuration.
using FluentAssertions;
using Xunit;
public class Example
{
[Fact]
public void Test()
{
var sourceArray = new[]
{
new Source { Name = "Foo" },
new Source { Name = "Bar" }
};
var destinationArray = sourceArray.MapToDestination();
destinationArray.Select(d => d.Name).Should().ContainInOrder(sourceArray.Select(o => o.Name));
var sourceList = new List<Source>
{
new() { Name = "Foo" },
new() { Name = "Bar" }
};
var destinationList = sourceList.MapToDestination();
destinationList.Select(d => d.Name).Should().ContainInOrder(sourceList.Select(o => o.Name));
}
private class Configuration : IMapConfig
{
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
.To<Destination>();
}
}
}
internal class Destination
{
public string Name { get; set; }
}
public class Source
{
public string Name { get; set; }
}
The Map
method is used to specify the object to map.
public void ConfigureMap(MapConfig config)
{
config.Map<Source>().To<Destination>();
}
π‘ See related tutorial.
The To
method is used to specify the object to map to.
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
.To<Destination>();
}
π‘ See related tutorial.
The From
method is used to specify the object to be mapped from.
public void ConfigureMap(MapConfig config)
{
config
.Map<Destination>()
.From<Source>();
}
π‘ See related tutorial.
The WithDestinationNamingConvention
method is used to specify naming convention rules to follow when attempting to
identify matched members between source
and destination
objects.
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
.To<Destination>()
.WithDestinationNamingConvention<PascalCaseConvention>();
}
π‘ See related tutorial.
The WithParameter
method is used to specify an argument or list of arguments that must be passed to perform
object mapping.
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
.To<Destination>()
.WithParameter<int>();
}
π‘ See related tutorial.
The ConstructFrom
method is used to specify how exactly the destination
object should be created.
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
.To<Destination>()
.ConstructFrom(source => new Destination { Name = "Name always Foo" });
}
π‘ See related tutorial.
The Finally
method is used to specify an action to be performed after the object is created.
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
.To<Destination>()
.Finally((source, destination) =>
{
source.Name = "Change source name";
destination.Name = "Change destination name";
});
}
π‘ See related tutorial.
The ForMember
method is used to specify an action to be performed on a member during mapping.
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
.To<Destination>()
.ForMember(member => member.Name, sourceValue => $"Copied from {sourceValue}");
}
π‘ See related tutorial.
The Ignore
method is used to specify a member to exclude from mapping.
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
.To<Destination>()
.Ignore(member => member.Name);
}
π‘ See related tutorial.
The WithMappingMode
method is used to specify object member types to include in the mapping while excluding others.
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
.To<Destination>()
.WithMappingMode(SourceMappingMode.MapMethods);
}
π‘ See related tutorial.
The [MapTo]
attribute is used to specify the object to map to.
[MapTo(typeof(Destination))]
public class Source
{
public string Name { get; set; }
}
π‘ See related tutorial.
The [MapFrom]
attribute is used to specify the object to map from.
[MapFrom(typeof(Source))]
internal class Destination
{
public string Name { get; set; }
}
π‘ See related tutorial.
The [Ignore]
attribute is used to specify a member to exclude from mapping.
[MapFrom(typeof(Source))]
internal class Destination
{
public string Name { get; set; }
[Ignore]
public int Year { get; set; }
}
The [Ignore]
attribute [For]
is used to specify the type from which the member mapping should be excluded.
[MapFrom(typeof(Source))]
internal class Destination
{
public string Name { get; set; }
[Ignore(For = typeof(Source))]
public int Year { get; set; }
}
π‘ See related tutorial.
The [DestinationMembersNamingConvention]
attribute is used to specify naming convention rules to follow when
attempting to identify matching members between source
and destination
objects.
[MapFrom(typeof(Source), DestinationMembersNamingConvention = typeof(PascalCaseConvention))]
internal class Destination
{
public string Name { get; set; }
}
π‘ See related tutorial.
The [DestinationConstructorNamingConvention]
attribute is used to specify naming convention rules that should be
followed when attempting to resolve constructor parameters.
[MapFrom(typeof(source),
DestMappingMode = DestMappingMode.MapToConstructor,
DestinationConstructorNamingConvention = typeof(CamelCaseConvention))]
internal class Destination
{
public Destination(string firstName)
{
FirstName = firstName;
}
public string FirstName { get; }
}
π‘ See related tutorial.
The [DestMappingMode]
attribute is used to specify which members of the destination
object should be mapped,
such as properties, fields, and methods.
[MapFrom(typeof(Source), DestMappingMode = DestMappingMode.MapToPropsAndFields)]
internal class Destination
{
public string Name { get; set; }
}
π‘ See related tutorial.
The [SourceMappingMode]
attribute is used to specify which members of the source
object to map, such as properties,
fields, and methods.
[MapTo(typeof(Destination), SourceMappingMode = SourceMappingMode.MapPropsAndFields )]
public class Source
{
public string Name { get; set; }
}
π‘ See related tutorial.
The IMapConfig
interface declares the method that must be implemented to specify the map configuration.
private class Configuration : IMapConfig
{
public void ConfigureMap(MapConfig config)
{
config
.Map<Source>()
.To<Destination>();
}
}
π‘ See related tutorial.
The IMappable
interface declares the method that must be implemented to specify the map configuration for the object.
internal class Destination : IMappable<Destination>
{
public string Name { get; set; }
public void ConfigureMap(MapConfig<Destination> map)
{
map.From<Source>();
}
}
π‘ See related tutorial.
- Add custom naming support
See open issues for a full list of proposed features (and known issues).
Distributed under the MPL 2.0 License. See LICENSE for details.
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". Don't forget to give the project a star! Thanks again!
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature
) - Commit your Changes (
git commit -m 'Add some AmazingFeature'
) - Push to the Branch (
git push source feature/AmazingFeature
) - Open a Pull Request