Skip to content

13 - Automapper

AutoMapper - What is it?

  • AutoMapper - convention-based object-to-object mapper that requires very little configuration.
  • Typically used on Domain to DTO transformation (and back).


  • When mapping gets complex – switch over to manual mapping

Installation

  • Install package
    AutoMapper.Extensions.Microsoft.DependencyInjection

Add as service in startup

1
2
// reference any class from class library to be scanned for mapper configurations
services.AddAutoMapper(typeof(DTO.App.ContactTypeDTO));

Use case

Domain entity

1
2
3
4
5
6
7
public class ContactType: DomainEntityId
{
    [MaxLength(32)] 
    public string ContactTypeValue { get; set; } = default!;

    public ICollection<Contact>? Contacts { get; set; }
}

Rest-api Json response

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[
    {
        "contactTypeValue": "Skype",
        "contacts": null,
        "id": "55855286-63ee-422c-f8cb-08d8ea5cdca5"
    },
    {
        "contactTypeValue": "Email",
        "contacts": null,
        "id": "34501db4-fa2d-42c9-7763-08d8eaa845d0"
    }
]

Usage

  • Define the DTO
  • Define configuration
  • Inherit from Profile – AutoMapper will find these via reflection
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class ContactTypeDTO 
{
    public Guid Id { get; set; }
    public string ContactTypeValue { get; set; } = default!;
}

public class ContactTypeDTOProfile: Profile
{
    public ContactTypeDTOProfile()
    {
        CreateMap<ContactType, ContactTypeDTO>();
    }
}

Map objects

1
2
3
4
5
6
7
8
[HttpGet]
[AllowAnonymous]
public async Task<ActionResult<IEnumerable<ContactTypeDTO>>> GetContactTypes()
{
    var data = await _uow.ContactTypes.GetAllAsync();
    var res = data.Select(d => _mapper.Map<ContactTypeDTO>(d));
    return Ok(res);
}

Result

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[
    {
        "id": "55855286-63ee-422c-f8cb-08d8ea5cdca5",
        "contactTypeValue": "Skype"
    },
    {
        "id": "34501db4-fa2d-42c9-7763-08d8eaa845d0",
        "contactTypeValue": "Email"
    }
]

AutoMapper – property names do not match?

Help mapper out

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class ContactType2DTO
{
    public Guid Id { get; set; }
    public string Value { get; set; } = default!;
}

public class ContactTypeDTOProfile : Profile
{
    public ContactTypeDTOProfile()
    {
        CreateMap<ContactType, ContactType2DTO>()
            .ForMember(
                dest => dest.Value,
                options => options
                    .MapFrom(src => src.ContactTypeValue)
            );
    }
}

Reverse mapping

If TypeA => TypeB has been configured, just add
.ReverseMap();
at the end of CreateMap<TypeA,TypeB> configuration.

What is going on?

AutoMapper uses a programming concept called Reflection to retrieve the type metadata of objects.
Reflection can be used to dynamically get the type from an existing object and invoke its methods or access its fields and properties.

Then, based on the conventions and configurations defined, AutoMapper can easily map the properties of the two types.

AutoMapper – Do’s

  • Use the AutoMapper.Extensions.Microsoft.DependencyInjection package in ASP.NET Core with services.AddAutoMapper(assembly[]). This package will perform all the scanning and dependency injection registration. You only need to declare the Profile configurations.
  • Always organize configuration into Profiles. Profiles allow us to group common configuration and organize mappings by usage.
  • Always use configuration options supported by LINQ over their counterparts as LINQ query extensions have the best performance of any mapping strategy.
  • Always flatten DTOs. AutoMapper can handle mapping properties A.B.C into ABC. By flattening our model, we create a more simplified object that won’t require a lot of navigation to get at data.
  • Always put common simple computed properties into the source model. Similarly, we need to place computed properties specific to the destination model in the destination model.

AutoMapper – Don’ts

  • Do not call CreateMap() on each request. It is not a good practice to create the configuration for each mapping request. Mapping configuration should be done once at startup.
  • Do not use inline maps. Inline maps may seem easier for very simple scenarios, but we lose the ease of configuration.
  • If we have to write a complex mapping behavior, it might be better to avoid using AutoMapper for that scenario.
  • Do not put any logic that is not strictly mapping behavior into the configuration. AutoMapper should not perform any business logic, it should only handle the mapping.
  • Avoid sharing DTOs across multiple maps. Model your DTOs around individual actions, and if you need to change it, you only affect that - action.
  • Do not create DTOs with circular associations. AutoMapper does support it, but it’s confusing and can result in quite bad performance. Instead, we can create separate DTOs for each level of a hierarchy we want.

Flattening

AutoMapper uses the following conventions:

  • It will automatically map properties with the same names.
  • If the source object has some association with other objects, then it will try to map with properties on the destination object whose name is a combination of the source class name and property name in the Pascal case.
    User.Address.City => UserDto.AddressCity
  • It will try to map methods on source object which has a Get prefix with a property on destination object with the name excluding the Get prefix.
    User.GetFullName() => UserDto.FullName

Complex maping

  • Hmmm, skip it. Really.
  • Nothing good will come out of it over the long run. Hard to debug, hard to test, easy to make mistakes which will come out only in manual testing.