A Map to Peak Perfomance: A Look at Mapping Library Performance in .NET 8

.Net 8

Introduction

When it comes to API development, something that often gets overlooked is the method of mapping data from the data source to DTOs. Why is this important? Well, there are a few reasons. For starters, we rarely want to expose the exact data and data structure from the data source (i.e. SQL database). Instead, it is much better to ‘map’ that data to the exact structure we want to expose.

Sometimes this mapping can be complex, and others can be near one-to-one with the data model. This is important because performance can vary wildly depending on the library and method used for this. In one of our current customer projects, we’ve started running into some performance issues caused by AutoMapper, the most popular .NET mapping library. It sparked inspiration to explore what other options were available.

 We are going to create a simple benchmarking application in .NET 8 using the BenchmarkDotNet benchmarking library and test the performance on the following libraries, along with manual mapping:

  • Automapper
  • Mapster
  • Mapperly

Table of Contents

Initial Setup in .Net 8

First, let’s go ahead and create a new console application in Visual Studio. For our purposes, we’ll be naming it MappingPerformanceTester. Ensure you select .NET 8.0 as the framework as well.

.NET 8.0 Visual Studio Screenshot

Next, we need to get our libraries installed. I like to use the Developer PowerShell within Visual Studio, but you can also just use the NuGet Package Manage. Here are the commands for convenience if you like the command line method:

				
					cdotnet add package Riok.Mapperly
dotnet add package AutoMapper
dotnet add package Mapster
dotnet add package BenchmarkDotNet

				
			

Now that we have everything installed, let’s set up some dummy objects we can use for our performance tests. For our testing, lets consider we have some data models for a zoo:

				
					    public class Zoo
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Location { get; set; }
        public IEnumerable<Enclosure> Enclosures { get; set; }
    }
    public class Enclosure
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Type { get; set; }
        public IEnumerable<Animal> Animals { get; set; }
    }
    public class Animal
    {
        public int Id { get; set; }
        public string Species { get; set; }
        public string Name { get; set; }
        public int Age { get; set; }
        public string HealthStatus { get; set; }
    }
    public class Staff
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Role { get; set; }
    }
				
			

This gives us a nice base we can test our mapping performance with, as we have some child-component relationships here.

Next, let’s create some DTOs that will serve as our target objects:

				
					    public class ZooGetDto
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Location { get; set; }
        public List<EnclosureSummaryGetDto> Enclosures { get; set; }
        public List<StaffInfoGetDto> StaffMembers { get; set; }
    }

    public class EnclosureSummaryGetDto
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Type { get; set; }
        public IEnumerable<AnimalSummaryGetDto> Animals { get; set; }
    }
    public class AnimalSummaryGetDto
    {
        public int Id { get; set; }
        public string Species { get; set; }
        public string Name { get; set; }
    }
    public class StaffInfoGetDto
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Role { get; set; }
    }

				
			

You can create these classes wherever, but to mimic an API project I went ahead and created new Models and DTOs folders and added them there:

DTOs Folder

Let’s go ahead and create a new class that will serve as our benchmarking executor. This will have all our testing setup and the benchmarks themselves. Let’s name it ‘ModelMapperBenchmark’:

				
					 [SimpleJob(RunStrategy.Throughput)]
 [KeepBenchmarkFiles(false)]
 [Orderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared)]
 public class ModelMapperBenchmark
 {
     private Zoo _zoo = CreateSampleZoo();
     private IMapper _autoMapper;
//… Future code will go here
}
				
			

 The first thing we need to do in this class is set up our mappers and initial model data. I created some mock data for our Zoo model object:

				
					 private static Zoo CreateSampleZoo()
 {
     // Create the zoo
     Zoo myZoo = new Zoo
     {
         Id = 1,
         Name = "Sunnydale Zoo",
         Location = "Sunnydale",
         Enclosures = new List<Enclosure>(),
         StaffMembers = new List<Staff>()
     };

     // Create and add enclosures
     Enclosure mammalEnclosure = CreateMammalEnclosure();
     Enclosure birdEnclosure = CreateBirdEnclosure();

     myZoo.Enclosures.Add(mammalEnclosure);
     myZoo.Enclosures.Add(birdEnclosure);

     // Add staff members
     Staff zookeeper = new Staff { Id = 1, Name = "John Doe", Role = "Zookeeper" };
     Staff veterinarian = new Staff { Id = 2, Name = "Jane Smith", Role = "Veterinarian" };

     myZoo.StaffMembers.Add(zookeeper);
     myZoo.StaffMembers.Add(veterinarian);

     return myZoo;
 }

 private static Enclosure CreateMammalEnclosure()
 {
     Enclosure mammalEnclosure = new Enclosure
     {
         Id = 1,
         Name = "Mammal House",
         Type = "Mammals",
         Animals = new List<Animal>
     {
         new Animal { Id = 1, Species = "Lion", Name = "Leo", Age = 5, HealthStatus = "Healthy" },
         new Animal { Id = 2, Species = "Elephant", Name = "Ella", Age = 10, HealthStatus = "Healthy" }
     }
     };

     return mammalEnclosure;
 }

 private static Enclosure CreateBirdEnclosure()
 {
     Enclosure birdEnclosure = new Enclosure
     {
         Id = 2,
         Name = "Bird Aviary",
         Type = "Birds",
         Animals = new List<Animal>
     {
         new Animal { Id = 3, Species = "Parrot", Name = "Polly", Age = 2, HealthStatus = "Healthy" },
         new Animal { Id = 4, Species = "Eagle", Name = "Eddie", Age = 4, HealthStatus = "Injured" }
     }
     };

     return birdEnclosure;
 }
				
			

AutoMapper

Setting up AutoMapper is very simple. We will put this set up code in our new ‘ModelMapperBenchmark’ class. We just need to configure the individual mappings between the models and DTOs:

				
					        [GlobalSetup]
        public void Setup()
        {
            var mapperConfig = new MapperConfiguration(cfg =>
            {
                cfg.CreateMap<Zoo, ZooGetDto>();
                cfg.CreateMap<Animal, AnimalSummaryGetDto>();
                cfg.CreateMap<Staff, StaffInfoGetDto>();
                cfg.CreateMap<Enclosure, EnclosureSummaryGetDto>();
            });

            _autoMapper = mapperConfig.CreateMapper();
        }
				
			

Now let’s create our benchmark test. Since this is the library our problematic project currently uses, I set the AutoMapper benchmark to be the baseline:

				
					        [Benchmark(Baseline = true)]
        public void AutoMapper()
        {
            _autoMapper.Map<ZooGetDto>(_zoo);
        }
				
			

Mapster

What’s great about Mapster is there is no setup! We can simply create our benchmark test:

				
					        [Benchmark]
        public void Mapster()
        {
            _zoo.Adapt<ZooGetDto>();
        }

				
			

Mapperly

There is some setup required for Maperly. We need to create a new mapping class for the configuration. I created a Mappers folder and created a new class called ‘ZooMapperlyMapper’:

				
					    [Mapper]
    public partial class ZooMapperlyMapper
    {
        public partial ZooGetDto Map(Zoo zoo);
    }
				
			

Add an instance of the new Mapperly mapper to our benchmarking class:

				
					public class ModelMapperBenchmark
 {
     private Zoo _zoo = CreateSampleZoo();
     private IMapper _autoMapper;
     private ZooMapperlyMapper _mapperlyMapper = new ZooMapperlyMapper();
//… rest of code
}
				
			

Now let’s add the actual benchmarking test:

				
					        [Benchmark]
        public void Mapperly()
        {
            _mapperlyMapper.Map(_zoo);
        }
				
			

Manual Mapping

The most tedious way to map a model to a DTO is to do it manually. Let’s create a ‘ZooManualMapper’ class and add the following manual mapping logic:

				
					    public static class ZooManualMapper
    {
        public static ZooGetDto MapToDto(Zoo zoo)
        {
            var dto = new ZooGetDto
            {
                Id = zoo.Id,
                Name = zoo.Name,
                Location = zoo.Location,
                Enclosures = new List<EnclosureSummaryGetDto>(),
                StaffMembers = new List<StaffInfoGetDto>()
            };

            foreach (var enclosure in zoo.Enclosures)
            {
                dto.Enclosures.Add(MapEnclosure(enclosure));
            }

            foreach (var staff in zoo.StaffMembers)
            {
                dto.StaffMembers.Add(MapStaff(staff));
            }

            return dto;
        }

        private static EnclosureSummaryGetDto MapEnclosure(Enclosure enclosure)
        {
            return new EnclosureSummaryGetDto
            {
                Id = enclosure.Id,
                Name = enclosure.Name,
                Type = enclosure.Type,
                Animals = MapAnimals(enclosure.Animals)
            };
        }

        private static List<AnimalSummaryGetDto> MapAnimals(List<Animal> animals)
        {
            var animalDtos = new List<AnimalSummaryGetDto>();
            foreach (var animal in animals)
            {
                animalDtos.Add(new AnimalSummaryGetDto
                {
                    Id = animal.Id,
                    Species = animal.Species,
                    Name = animal.Name
                });
            }
            return animalDtos;
        }

        private static StaffInfoGetDto MapStaff(Staff staff)
        {
            return new StaffInfoGetDto
            {
                Id = staff.Id,
                Name = staff.Name,
                Role = staff.Role
            };
        }
    }
				
			

After that, we can add our manual benchmarking test back in our benchmarking class:

				
					        [Benchmark]
        public void Manual()
        {
            ZooManualMapper.MapToDto(_zoo);
        }
				
			

Finishing Up

Now in our original Program class, lets add the code to start our tests!

				
					using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;
using MappingPerformanceTester;

var config = DefaultConfig.Instance.WithOptions(ConfigOptions.DisableOptimizationsValidator);
BenchmarkRunner.Run<ModelMapperBenchmark>(config);
Console.ReadLine();
				
			

Results

For running the application, ensure your running under ‘Release’ mode. This will ensure the tests are as accurate as possible. When ready, go ahead and run the program and wait for it to finish. When its done, you should see results like those below. Yours might differ slightly based on your computer hardware:

Mapperly Manual AutoMapper Mapster Results chart

From our results, Mapperly is by far the fastest mapping technique used here. It’s even faster than manually mapping and four times faster than AutoMapper! Mapping complexly structured objects can lead to a massive performance boost. You can even visualize this below on the following graph:

Mapperly, Manual, Automapper, Mapster performance comparison graph

 

This shows the importance of doing due diligence when choosing 3rd party libraries to incorporate into your project. We would have made different choices if we had known about this performance initially.