Skip to content

Configuration

Mohammadreza edited this page Apr 5, 2024 · 18 revisions

Property Mapping

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; }
}

Ignore Property

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; }
}

Property Type Conversion

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.
}

Before or After Mapping Events

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 };
}

Constructor Mapping

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);
}

Collections

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[].

Null Handling

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 in MapPropertyAttribute, MapFromAttribute or at the assembly level in the csproj or .editorconfig file.

Flattening

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.

Polymorphic Mapping

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();
}

Enum Mapping

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.

Enum Mapping Strategies

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.

Fallback Value

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.

Strict Enum Mapping

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.

Ignore Enum Member

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.

Projection (Collections)

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 IQueryables is not supported.

Automatic Projection

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> and List<T>.

Manual Projection

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);
}