Mapping Objects in C#

In this post, I’m going to talk about different libraries available for mapping one class to another in C#.

 

1 - What does mapping mean?

Well, mapping, or object mapping, is simply taking an object and transforming it into another one.

In C#, like many other languages, there are libraries that let us do this automatic mapping, since by default, it’s not possible.

 

In this post we’re going to see two, but, if you’re not familiar, there are actually dozens of libraries out there.

 

 

2 - Automapper library in C#

Automapper is probably the most famous mapping library in C#. This library was essentially created to save developers from tedious and boring manual mapping tasks.

 

For example, when we have DTOs and entities or domain objects, we have to map practically 1-to-1, and as I said, it’s boring and tedious.

That’s why automapper was born and became so popular.

 

2.1 - Examples and use cases of automapper in C#

To see an example, the simplest case is mapping Tipo A to Tipo B; by default, we barely need any configuration.

  • Note: if the properties have the same name, they are mapped correctly without any extra configuration

 

In our case, we have two types, a DTO and an Entity:

public record UserDto(string UserName, string Email);    
public record UserEntity(string UserName, string Email);

Now we simply instantiate our mapper and use it, so you can see how it works:

[Fact]
public void Test_MapDtoToEntity()
{
    MapperConfiguration config = new (cfg => 
        cfg.CreateMap<UserDto, UserEntity>());
    
    IMapper mapper = new Mapper(config);

    UserDto dto = new UserDto("username", "[email protected]");
    
    UserEntity entity = mapper.Map<UserEntity>(dto);
    
    Assert.Equal(dto.Email, entity.Email);
    Assert.Equal(dto.UserName, entity.UserName);

}

In this example, we check that both Email and UserName are transferred from one to the other correctly.

 

 

If this was a real application, what you’d do is set up all mappings at the start and add them into the dependency container and inject them via IMapper.

 

As you saw with CreateMap, you create a mapping, but with CreateMap<T, U>().ReverseMap(), you create the reverse. Obviously, you could use another CreateMap, no problem, but, generally, in apps it’s done with ReverseMap, since that’s less code.

[Fact]
public void Test_MapDtoToEntity_Reverse()
{
    MapperConfiguration config = new (cfg => 
        cfg.CreateMap<UserDto, UserEntity>().ReverseMap());
    
    IMapper mapper = new Mapper(config);

    UserEntity entity = new UserEntity("username", "[email protected]");
    
    UserDto dto = mapper.Map<UserDto>(entity);
    
    Assert.Equal(entity.Email, dto.Email);
    Assert.Equal(entity.UserName, dto.UserName);
}

Of course, this is the simplest case, but we can include rules, for example, if a property isn’t the same: instead of user and email, we have a user with contact details:

public class UserDetailsDto
{
    public string UserName { get; set; }
    public ContactDetails ContactDetails { get; set; }
}

public class ContactDetails
{
    public string Email { get; set; }
    public string PhoneNumber { get; set; }
}

public class UserDetailsEntity
{
    public string UserName { get; set; }
    public string Email { get; set; }
}

As you can see, we went from using records to using classes with getters and setters. This is needed because of the "magic" happening behind the scenes (which we'll explain in a moment), but leaving getters and setters open isn’t the best idea, since your code will be mutable and, in my opinion, it’s best to have immutable code.

 

Anyway, to map a complex object, we have to include a rule, because that mapping for obvious reasons won’t happen automatically. We use `ForMember` for this rule.

[Fact]
public void Test_MapComplexObject()
{
    MapperConfiguration config = new(cfg =>
        cfg.CreateMap<UserDetailsDto, UserDetailsEntity>()
            .ForMember(destination => destination.Email,
                options => options
                        .MapFrom(source => source.ContactDetails.Email)));

    IMapper mapper = new Mapper(config);

    UserDetailsDto dto = new UserDetailsDto()
    {
        ContactDetails = new ContactDetails()
        {
            Email = "[email protected]",
            PhoneNumber = "1256555"
        },
        UserName = "username"
    };

    UserDetailsEntity entity = mapper.Map<UserDetailsEntity>(dto);

    Assert.Equal(dto.ContactDetails.Email, entity.Email);
    Assert.Equal(dto.UserName, entity.UserName);
}

 

 

2.2 - The magic behind automapper, how does it actually work?

If you’re new to C# programming, you’re probably wondering how this works, since it looks like magic: you do something that should be long and tedious in seconds and it seems to work well.

The answer is that Automapper uses what’s called Reflection. It lets you read object metadata and dynamically see what they’re like, extract info, assign values, etc.

 

At first glance, this sounds great, but the truth is, using reflection – especially with big objects – isn’t recommended, since it’s slow and expensive. If you’re doing just a few calls, you won’t notice anything, but if your application uses Automapper for thousands of calls per minute, performance will degrade drastically.

 

 

3 - Introduction to Mapperly in C#

The second library I want to present today is Mapperly, which is not that well known because it's pretty new. Also, it only works on net 7 or above, since it uses Source Generators.

 

For those who don’t know, source generators in C# generate code at compile time, which means, unlike before where we used reflection, in this case we’re using "regular" C# code.

If there’s an error in your mapping, you’ll see it at compile time, not runtime.

 

3.1 - Mapperly examples with C#

The use case is exactly the same as before: we map an object 1-to-1, but the configuration is a bit different.

You have to create a partial class with the mapper and indicate the Mapper attribute, so the source generator knows this code should be converted internally to the mapper code.

[Mapper]
public partial class UserMapper
{
    public partial UserEntity ToEntity(UserDto dto);
}

Then we instantiate the mapper and call the method we created:

[Fact]
public void Test_MapDtoToEntity()
{
    UserMapper mapper = new UserMapper();
    UserDto dto = new UserDto("username", "[email protected]");

    UserEntity entity = mapper.ToEntity(dto);

    Assert.Equal(dto.Email, entity.Email);
    Assert.Equal(dto.UserName, entity.UserName);
}

To do the reverse, the same idea: in the same mapper, a method to convert to DTO and in the test, just call that method:

public partial class UserMapper
{
    public partial UserEntity ToEntity(UserDto dto);
    public partial UserDto ToDto(UserEntity entity);
}

[Fact]
public void Test_MapEntityToDto()
{
    UserMapper mapper = new UserMapper();
    UserEntity entity = new UserEntity("username", "[email protected]");

    UserDto dto = mapper.ToDto(entity);

    Assert.Equal(entity.Email, dto.Email);
    Assert.Equal(entity.UserName, dto.UserName);
}

Now let’s take a look at complex objects, just like before; in this case, you also need to configure the mapping, though it works a bit differently:

[Mapper]
public partial class UserDetailsMapper
{   
    [MapProperty(nameof(@UserDetailsDto.ContactDetails.Email), nameof(@UserEntity.Email))]
    public partial UserEntity ToEntity(UserDetailsDto dto);
}

[Fact]
public void Test_MapComplexDtoToEntity()
{
    UserDetailsMapper mapper = new UserDetailsMapper();
    UserDetailsDto dto = new UserDetailsDto()
    {
        ContactDetails = new ContactDetails()
        {
            Email = "[email protected]",
            PhoneNumber = "1256555"
        },
        UserName = "username"
    };

    UserEntity entity = mapper.ToEntity(dto);

    Assert.Equal(dto.ContactDetails.Email, entity.Email);
    Assert.Equal(dto.UserName, entity.UserName);
}

As we see, it’s also done with an attribute, where we specify “from where” to “destination.” Plus, the @ symbol allows the sourceGenerator to take the full namespace; without it, you’d have to write the complete namespace of the class as text. With the @ it’s much easier to see and maintain.

Still, if you have a lot of mappings, maintenance gets tough.

 

This is just an introduction. If you want more info, for example, how to use static mappers with extension methods, I recommend you read their documentation.

 

 

4 - My personal conclusion

Personally, I’m not a huge fan of mappers. Mapperly changed my mind a bit, but my aversion comes from a job I once had where objects were very large and the configurations were endless and hard to maintain, so a lot of time was lost.

Mapperly makes both mapping and mapper configuration a bit simpler than automapper, and it solves all the performance issues that automapper had (plus, it works with records).

 

But in both cases, if the object is big and has many configurations, it’s still going to be very difficult to maintain. In recent months, the "rule" I’ve followed is: if it has three or fewer configurations, I use Mapperly; if it has more than three, I prefer to create the mapping myself, and honestly, this rule has worked well for me.

 

Finally, I’ll say the main problem is people don’t test the mapping, whether it’s hand-written, Automapper, or Mapperly, basically NOBODY writes tests against the mapper, which is where a lot of headaches come from.

 

This post was translated from Spanish. You can see the original one here.
If there is any problem you can add a comment bellow or contact me in the website's contact form

Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2025 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café