Mapping Objects in C#

In this post, I will talk about different libraries we have to map one class to another in C#.

 

1 - What is mapping?

Well, mapping, or object mapping, or whatever you want to call it, is nothing more than taking one object and converting it into another different one.

In C#, as in many other languages, we have libraries that allow us to automatically map objects, since by default it is not possible.

 

In this post we are going to see two, but if you're not familiar, you should know that there are dozens of libraries out there.

 

 

2 - Automapper Library in C#

Automapper is possibly the most famous mapping library in C#. This library was basically created to allow developers to avoid doing the mapping manually, as it’s a very tedious and boring task.

 

For example, when we have DTOs and entities or domain objects, we need to map those objects almost one to one, and as I said, it's boring and tiresome.

That's why automapper was born and became popular.

 

2.1 - Automapper examples and use cases in C#

To see an example, the simplest use case of automapper is to map Type A to Type B and by default, we don’t really need any configuration.

  • Note: If the properties have the same name, they are mapped correctly without configuration.

 

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

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

Now we just need to instantiate our mapper and use it, so we can see that it works:

[Fact]public void Test_MapDtoToEntity(){    MapperConfiguration config = new (cfg =>         cfg.CreateMap());        IMapper mapper = new Mapper(config);    UserDto dto = new UserDto("username", "[email protected]");        UserEntity entity = mapper.Map(dto);        Assert.Equal(dto.Email, entity.Email);    Assert.Equal(dto.UserName, entity.UserName);}

In this example, we are checking that both Email and UserName are properly passed from one to the other.

 

 

If this were a real application, what we would do is instantiate all the maps at startup and include them in the dependency container and inject it through the IMapper.

 

As you have seen, with CreateMap we create a mapping, but with CreateMap<T, U>().ReverseMap() we create the reverse mapping. Of course, you could use another CreateMap and there would be no problem, but generally in apps it’s done with ReverseMap as it's less code to write.

[Fact]public void Test_MapDtoToEntity_Reverse(){    MapperConfiguration config = new (cfg =>         cfg.CreateMap().ReverseMap());        IMapper mapper = new Mapper(config);    UserEntity entity = new UserEntity("username", "[email protected]");        UserDto dto = mapper.Map(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 in other cases where a property is not 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 switched from using records to using classes with getters and setters, which is necessary for the “magic” that happens behind the scenes (which we will now explain). However, leaving the getters and setters open is not the best idea, as the code becomes mutable, and in my opinion, it is better to have immutable code.

 

But anyway, let’s continue. In this case, to map a complex object, we have to include a rule because, for obvious reasons, that mapping is not going to happen automatically. To add a rule we must use ForMember.

[Fact]public void Test_MapComplexObject(){    MapperConfiguration config = new(cfg =>        cfg.CreateMap()            .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(dto);    Assert.Equal(dto.ContactDetails.Email, entity.Email);    Assert.Equal(dto.UserName, entity.UserName);}

 

 

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

If you’re new to programming in C#, you're probably wondering how this works, as it almost feels like magic. It allows you to program something that would otherwise be long and tedious in seconds, and it seems to work well.

The answer is that Automapper uses what is called Reflection, which allows us to read the metadata of objects and dynamically see what they are like, extract information, assign values, etc.

 

At first sight it looks very nice, but the reality is that doing reflection, especially with big objects, is not very recommended, since it is very slow and very expensive. If you're only going to use it with a few calls, you won't notice any problems, but on the other hand, if the application using Automapper makes thousands of calls per minute, performance is going to drop drastically.

 

 

3 - Introduction to Mapperly in C#

The second library I want to introduce today is Mapperly, which is not very well known because it is fairly new. Also, it only works in .NET 7 or above, and that’s because it uses Source Generators.

 

In case you don't know, source generators in C# are code that generate other code during compile time. This means, unlike the previous case where we used reflection, here we use regular C# code.

And if we make an error in the mapping, we will get the error at compile time, not at runtime.

 

3.1 - Mapperly examples with C#

As a use case, we have exactly the same as before. We are going to map an object one to one. The configuration is a little different though.

We have to create a partial class for the mapper and indicate the Mapper attribute so the source generator knows that this code has to be converted internally into the mapping code.

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

Then we need to instantiate the mapper and call the method we have 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, you do the same thing: in the same mapper, add a method that converts to DTO and in your test, just call the 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 move on to complex objects as we saw before. In this case, we also have to configure the mapping needed, though it's a little different:

[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 you can see, this is also done with an attribute where we specify the "from" and "to". The @ symbol allows the source generator to take the entire namespace; if we don't put it, we have to write the full class namespace as text. Using the at sign makes it much easier to read and maintain.

Still, if you have many mappings, it will be very difficult to maintain.

 

This is just an introduction; if you want more information, like how to use static mappers with extension methods, I recommend checking out their documentation.

 

 

4 - My personal conclusion

Personally, I am not a fan of mappers; since Mapperly, I like them a bit more, but my aversion comes from a job where there were very large objects and the configurations were endless and very hard to maintain, resulting in a huge waste of time.

Mapperly makes both the mappings and the way we configure mappers easier than automapper, plus it solves all the performance issues automapper had (and it works with records as well).

 

However, in both cases, if the object is large and has many configurations it will be very hard to maintain. In the last few months, the "rule" I have followed is: if it has 3 or fewer configurations, I can use Mapperly; if it has more than 3, I have to create the mapping myself, and to be honest, I am happy with this rule.

 

To finish, I’ll say that the main problem is people don’t test their mappings, whether it’s handwritten, done by Automapper, or Mapperly. Virtually NOBODY writes tests against the mapper, and that’s where tons 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

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

Buy me a coffee Invitame a un café