-
Notifications
You must be signed in to change notification settings - Fork 6
Configuration
By default, MapTo
maps properties from the target class with the same name (case-sensitive) as those in the source class. However, sometimes, we need to map a property to another one with a different name. In such cases, we can use the MapProperty
attribute to annotate the property in the target class and indicate which property in the source class should be mapped to it.
[MapFrom(typeof(User))]
public partial class UserViewModel
{
...
[MapProperty(From = nameof(User.LastName))]
public string Surname { get; }
}
To prevent a property from being mapped, we can use the IgnoreProperty
attribute to annotate the property in the target class.
[MapFrom(typeof(User))]
public partial class UserViewModel
{
...
[IgnoreProperty]
public int Age { get; }
}
Sometimes, we need to convert the type of a property while mapping. For example, we may need to convert a DateTime
property to a string
property or vice versa. In such cases, we can use the PropertyTypeConverter
attribute.
The PropertyTypeConverter
attribute accepts the name of a static method with the following signature defined in the target class or any other class that it has access to and calls it to convert the property value.
static <TargetPropertyType> <MethodName>(<SourcePropertyType> value, object[]? parameters);
// OR
static <TargetPropertyType> <MethodName>(<SourcePropertyType> value);
[MapFrom(typeof(User))]
public partial class UserViewModel
{
...
[MapProperty(From = nameof(User.Id))]
[PropertyTypeConverter(nameof(Converters.IntToHexConverter))]
public string Key { get; }
}
internal static class Converters
{
public static string IntToHexConverter(int source) => $"{source:X}"; // The converter method.
}
Occasionally, we may need to perform some actions before or after mapping. For example, we may need to set a property value before mapping, or we may need to perform some actions after mapping. In such cases, we can use the BeforeMap
and AfterMap
properties of the MapFrom
attribute and register the static
methods we want to be called before or after mapping.
[MapFrom(typeof(User), BeforeMap = nameof(BeforeMap), AfterMap = nameof(AfterMap))]
public partial class UserViewModel
{
...
private static User? BeforeMap(User? source) { ... } // The method to be called before mapping.
private static void AfterMap(UserViewModel target) { ... } // The method to be called after mapping.
}
You can define the before and after mapping methods in the target type class or any other method that target type has access to with the following signatures:
static void BeforeMap(<SourcePropertyType>? source);
static <SourcePropertyType>? BeforeMap(<SourcePropertyType>? source);
static void AfterMap(<TargetPropertyType> target);
static <TargetPropertyType> AfterMap(<TargetPropertyType> target);
static void AfterMap(<SourcePropertyType> source, <TargetPropertyType> target);
static <TargetPropertyType> AfterMap(<SourcePropertyType> source, <TargetPropertyType> target);
💡 If any of the above attributes require a method,
MapTo
will guide you with appropriate compiler warnings or errors to help with the method declaration.
The BeforeMap
method can either be void
or return the source
type. If it returns the source
type, the returned value will be used for mapping instead of the original source
value. If you return null
in the BeforeMap
method, the mapping will be skipped, and null
will be returned as the result of the mapping operation.
You can use the AfterMap
method to perform any actions after mapping. It can either be void
or return the target
type. If it returns the target
type, the returned value will be used as the result of the mapping operation instead of the original target
value.
public class OrderLine
{
public int Amount { get; set; }
public decimal UnitPrice { get; set; }
}
[MapFrom(typeof(OrderLine), AfterMap = nameof(CalculateTotalPrice))]
public class OrderLineViewModel
{
public int Amount { get; }
public decimal UnitPrice { get; }
public decimal TotalPrice { get; private set; }
private static void CalculateTotalPrice(OrderLineViewModel target) =>
target.TotalPrice = target.Amount * target.UnitPrice;
}
[MapFrom(typeof(OrderLine), AfterMap = nameof(CalculateTotalPrice))]
public record OrderLineRecord(int Amount, decimal UnitPrice)
{
public decimal TotalPrice { get; private set; }
private OrderLineRecord CalculateTotalPrice() =>
this with { TotalPrice = Amount * UnitPrice };
}
MapTo
can also automatically generate a constructor for read-only properties. To enable this feature, we must declare the target class as partial
and declare the required properties only with get
accessors.
[MapFrom(typeof(User))]
public partial class UserViewModel
{
public int Id { get; }
public string FirstName { get; }
public string LastName { get; }
}
This will generate the following constructor for the UserViewModel
class.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("MapTo", "0.9.1.25168")]
public partial class UserViewModel
{
public UserViewModel(int id, string firstName, string lastName)
{
Id = id;
FirstName = firstName;
LastName = lastName;
}
}
It also generates the following extension method for the User
class and uses the constructor to map the properties.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("MapTo", "0.9.1.25168")]
public static class UserMapToExtensions
{
[return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull("user")]
public static UserViewModel? MapToUserViewModel(this User? user)
{
if (ReferenceEquals(user, null))
{
return null;
}
return new UserViewModel(user.Id, user.FirstName, user.LastName);
}
}
However, sometimes, we need a secondary constructor, and it needs to be clarified which constructor should be used for mapping. In such cases, we can use the MapConstructor
attribute to annotate the constructor we want to be used for mapping.
[MapFrom(typeof(User))]
public class UserViewModel
{
public UserViewModel(int id, string firstName, string lastName) =>
(Id, FirstName, LastName) = (id, firstName, lastName);
[MapConstructor]
public UserViewModel(string firstName, string lastName)
: this(default, firstName, lastName) { }
public int Id { get; }
public string FirstName { get; }
public string LastName { get; }
}
Having any constructors in the target class will stop the automatic constructor generation. And using the MapConstructor
attribute will result in the following extension method for the User
class.
[return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull("user")]
public static UserViewModel? MapToUserViewModel(this User? user)
{
if (ReferenceEquals(user, null))
{
return null;
}
return new UserViewModel(user.FirstName, user.LastName);
}
MapTo
can also map any generic type of list, collection and array, and it only requires annotating the element type of the collection with the MapFrom
attribute.
[MapFrom(typeof(Comment))]
public partial class CommentViewModel
{
public string Text { get; }
}
[MapFrom(typeof(User))]
public partial class UserViewModel
{
...
public IEnumerable<CommentViewModel> Comments { get; }
}
The source and destination types of the collection can be different. For example, we can map a List<User>
to an IReadOnlyCollection<UserViewModel>
or IEnumerable<T>
, IList<T>
or even T[]
.
By default, MapTo
will handle nullable reference types differently, depending on their nullability annotations.
When the nullability context is disabled
, MapTo will ignore all null checks, and the nullability of the target values will be the same as the source property. When the nullability context is enabled
, MapTo will follow these rules:
- If both the target and source are nullable, it will assign the target property the same value as the source property without performing any null checks.
- If the target is nullable, but the source is not, it will assign the target property the same value as the source property without performing any null checks, unless there is a PropertyTypeConverterAttribute on the target property. In this case, MapTo will use the nullability context of the converter's return value.
- If the target is not nullable, but the source is, MapTo will assign an empty collection to the target property if it is a collection. Otherwise, it will report a diagnostic error.
- If both the target and source are not nullable, MapTo will assign the target property the same value as the source property without performing any null checks.
You can also change this behaviour by changing the
NullHandling
option inMapPropertyAttribute
,MapFromAttribute
or at the assembly level in thecsproj
or.editorconfig
file.
Object mapping is particularly handy when we need to convert a complex model to a flattened model that is more suitable for different use cases such as data binding, serialization, and more. For instance, suppose we want to return the following model as a JSON object in an API response. In that case, we can flatten it like this:
public record Order(Customer customer)
{
private readonly List<OrderLineItem> _orderLineItems = new List<OrderLineItem>();
public IReadOnlyCollection<OrderLineItem> OrderLineItems => _orderLineItems;
public void AddOrderLineItem(Product product, int quantity) =>
_orderLineItems.Add(new OrderLineItem(product, quantity));
public decimal GetTotal() => _orderLineItems.Sum(oli => oli.GetTotal());
}
public record Product(string Name, decimal Price);
public record Customer(string Name);
public record OrderLineItem(Product Product, int Quantity)
{
public decimal GetTotal() => Quantity * Product.Price;
}
We can flatten the above model to the following model:
[MapFrom(typeof(Order))]
public class OrderResponse
{
[MapProperty(From = nameof(Order.Customer.Name))]
public string CustomerName { get; }
// We can map a property to a property or a parameterless method in the source class.
[MapProperty(From = nameof(Order.GetTotal))]
public decimal Total { get; }
}
Currently, MapTo does not support reverse mapping and unflattening and requires manual creation of the reverse map. However, this feature will be implemented in future releases.
MapTo
can also map polymorphic types and inherently use any mapping configuration defined for the base type.
[MapFrom(typeof(Employee))]
public partial class EmployeeViewModel
{
public int Id { get; init; }
public string EmployeeCode { get; set; }
}
[MapFrom(typeof(Manager))]
public partial class ManagerViewModel : EmployeeViewModel
{
public ManagerViewModel(int id) => Id = id;
public int Level { get; init; }
public List<EmployeeViewModel> Employees { get; set; } = new();
}
Sometimes, we don't necessarily want to use the same enum type in the source and target classes. For example, we may want to use a different name in the target enum, use only some values of the source enum, or even use a different type for the target enum. We can use the MapFrom
attribute to configure the enum mapping in such cases.
The enum mapping configurations can be defined at the target enum itself, the target class to control all enums in the class, or the project level via project properties or the .editorconfig
file.
We can control how the enum members are mapped by setting the EnumMappingStrategy
property of the MapFrom
attribute. The following enum mapping strategies are supported:
Strategy | Description |
---|---|
ByVal | Maps the enum values by their underlying values. This is the default strategy. |
ByName | Maps the enum values by their names. This strategy is case-sensitive. |
ByNameCaseInsensitive | Maps the enum values by their names, ignoring the case. |
By default, if the source enum value does not exist in the target enum, MapTo
will throw an exception. However, we can use the EnumMappingFallbackValue
property of the MapFrom
attribute to specify a fallback value to be used instead.
By default (StrictEnumMapping.Off
), MapTo
does not enforce any restrictions on mapping enums. However, we can use the StrictEnumMapping
property of the MapFrom
attribute to enforce strict enum mapping. The following strict enum mapping options are supported:
Option | Description |
---|---|
Off | No restrictions on enum mapping. This is the default option. |
SourceOnly | Enforce all source enum members to be mapped to target enum members. |
TargetOnly | Enforce all target enum members to be mapped from source enum members. |
SourceAndTarget | Enforce all source enum members to be mapped to target enum members and all target enum members to be mapped from source enum members. |
We can use the IgnoreEnumMember
attribute to ignore a specific enum member from mapping. This attribute can be used to ignore both source and target enum members, and similar to the MapFrom
attribute, it can be used at the enum itself, the target class or the project level. This attribute is especially useful when StrictEnumMapping
is enabled.
MapTo
can map collections to other collections. This feature is particularly useful when we want to map a collection of entities to a collection of view models or DTOs.
💡At the moment, mapping
IQueryable
s is not supported.
When mapping an iterable type to the same iterable type, MapTo
can automatically generate projection extension methods. You can configure which types should be automatically projected by setting the ProjectTo
property of the MapFrom
attribute. For example, the following code will generate the IEnumerable<UserViewModel> MapToUserViewModels(this IEnumerable<User>)
and List<UserViewModel> MapToUserViewModels(this List<User>)
extension methods.
[MapFrom(typeof(User), ProjectTo = ProjectionType.IEnumerable | ProjectionType.List)]
public class UserViewModel
{
public int Id { get; }
public string FirstName { get; }
public string LastName { get; }
}
By default,
MapTo
will generate the projection extension methods for arrays,IEnumerable<T>
andList<T>
.
We can also manually project most of the .NET's built-in iterable type to another iterable type by creating a static partial
method in the target class. This is particularly useful when we want to have different method name or the parameter and return types are different iterable types. For example, the following code will generate the MapToUserViewModels
extension method for the IEnumerable<User>
to List<User>
projection.
[MapFrom(typeof(User))]
public class UserViewModel
{
public int Id { get; }
public string FirstName { get; }
public string LastName { get; }
private static partial List<UserViewModel> ToUserViewModels(IEnumerable<User> users);
}