DEV Community

Cover image for AutoMapper in .NET: When You May Not Need It
StepOne
StepOne

Posted on

AutoMapper in .NET: When You May Not Need It

AutoMapper can remove repetitive member-to-member assignments, but it also moves mapping behavior away from ordinary C# code. That trade affects navigation, static analysis, debugging, performance, trimming, and how quickly a reviewer can understand a conversion.

This article examines the problem AutoMapper was originally designed to solve, its own usage guidelines, and the costs that appear when convention-based mapping crosses architectural boundaries. The goal is not a blanket ban; it is a code-review test for deciding whether the abstraction earns its place in a particular .NET application.


You can often find opinions like this online:

He has a point!

He has a point!

Or cries for help like this:

This is how a typical AutoMapper thread on Reddit begins

This is how a typical AutoMapper thread on Reddit begins.

Let us start by answering one question:

What AutoMapper Was Designed to Do

It is worth considering the period when the library was born. It was the late 2000s, and the era of MVC frameworks had only just begun. The following had already appeared:

  • Ruby on Rails

  • Django

  • ASP.NET MVC

Unlike the first two, Microsoft's framework offered no advice or guidance on what the letter "M" was supposed to mean. Developers were free to design models however they liked, which made the choice difficult. Should the model be an entity? A data access object? Would a DTO work?

Every development team therefore established its own rules for creating models. Jimmy's team chose models tied to views, producing a kind of ViewModel—not to be confused with MVVM. Their rules were as follows:

  1. All views are strongly typed.

  2. The ViewModel-to-View relationship is one-to-one: every View has its own ViewModel.

  3. The View defines the structure of the ViewModel. The ViewModel receives only what the View needs to display.

  4. The ViewModel contains only data and behavior related to its View.

After the team had built a couple of dozen screens, the amount of boilerplate inevitably became a problem. Eventually, they also realized that the presentation types were merely subsets of their domain types.

Null reference exceptions kept appearing, the tests were painful to write, and even more screens were planned—perhaps as many as a thousand. AutoMapper was born to clean up this mess.

AutoMapper's Philosophy

The new tool did three things:

  • Required destination types to follow a convention;

  • Hid null reference exceptions as thoroughly as possible;

  • Made this functionality easy to test.

What assumptions did this design make? No one can explain them better than the library's author:

AutoMapper works because it enforces a convention. It assumes that your destination types are a subset of the source type. It assumes that everything on your destination type is meant to be mapped. It assumes that the destination member names follow the exact name of the source type. It assumes that you want to flatten complex models into simple ones.

All of these assumptions come from our original use case - view models for MVC, where all of those assumptions are in line with our view model design. With AutoMapper, we could enforce our view model design philosophy.

And this is why our usage of AutoMapper has stayed so steady over the years - because our design philosophy for view models hasn't changed.

To summarize, AutoMapper is a tool for solving a specific problem within a particular project-wide development approach. For it to work as intended, your custom data types must follow an external convention.

That leads to the obvious question:

When AutoMapper Fits a .NET Project

I suspect most negative posts about the library exist because the answer was "no," but nobody asked the question.

That is how you end up with situations such as putting a password-generation algorithm into a mapping configuration.

I don't think this code should be shown as text. Heaven forbid someone copies it

I don't think this code should be shown as text. Heaven forbid someone copies it.

Jimmy Bogard provides a dedicated checklist that you can use to determine whether you are using the library correctly.

Even when you use it as intended, however, it is worth noting that:

AutoMapper Drawbacks in Large .NET Codebases

Let us make that case point by point.

Misleading Static Analysis

The static analyzer only tells you that some model fields are not used at all. You can mark them with [UsedImplicitly], but that merely sidesteps the problem.

Code that contains no business logic and only declares data passed back and forth cannot safely be deleted based on an IDE suggestion. Doing so will break the application, and you will discover that only at runtime.

The static analyzer is supposed to guard order and quality in the project, yet it cannot help. This third-party library reduces your confidence in its reports.

Poor Code Navigation

There is no way to determine which entity field maps to a particular DTO field. The "show usages" action shows nothing but the field declaration.

After all, AutoMapper works implicitly.

You could solve the problem by writing explicit mapping configurations. But if you do that, do you still need this entire reflection-based mechanism?

Difficult Debugging

In some cases, debugging becomes practically impossible.

With implicit mapping, something happens under the hood of the library and the developer cannot trace it. All they can do is inspect the result and compare it with the expected behavior.

What if we explicitly define a mapping configuration? Roughly speaking, that means juggling calls to ForMember and MapFrom. Consider the signature of one of them:

void MapFrom<TMember>(Expression<Func<TSource, TMember>> sourceMember);
Enter fullscreen mode Exit fullscreen mode

This is not executable code but code that describes behavior: the argument is an expression, not a delegate. There is nowhere to put a breakpoint, and you cannot catch an exception there either. For example, suppose we have two models, UserEntity and UserDTO:

public class UserEntity
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string City { get; set; }
}

public class UserDTO
{
    public string FullName { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Now we provide this mapping configuration:

Mapper.Initialize(cfg =>
{
    cfg.CreateMap<UserEntity, UserDTO>()
        .ForMember(
            x => x.FullName,
            opt => opt.MapFrom(x => $"{x.FirstName} {x.LastName} ({x.Address.City})")
        );
});
Enter fullscreen mode Exit fullscreen mode

Then we pass an object that should certainly trigger an NRE:

var userEntity = new UserEntity()
{
    FirstName = "Cezary",
    LastName = "Piątek",
    Address = null,
};
var userDto = Mapper.Map<UserDTO>(userEntity);
Console.WriteLine(JsonConvert.SerializeObject(userDto, Formatting.Indented));
Enter fullscreen mode Exit fullscreen mode

Instead of an NRE, we get:

{
    "FullName": null
}
Enter fullscreen mode Exit fullscreen mode

The library's pervasive use of expressions and reflection creates another interesting case. Consider this code:

using System;
using AutoMapper;

var config = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<UserSource, UserDestination>()
        .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name.ToLower()));
});
var mapper = config.CreateMapper();
var source = new UserSource("VASYA");
var destination = mapper.Map<UserSource, UserDestination>(source);
Console.WriteLine(destination);

public record UserSource(string Name);

public record UserDestination(string Name);
Enter fullscreen mode Exit fullscreen mode

When destination is constructed, the Name field is mapped, but ToLower is not applied:

{
    "UserDestination": {
        "Name" : "VASYA"
    }
}
Enter fullscreen mode Exit fullscreen mode

Broken Code Organization

Simple commercial projects do not exist. They may look simple at first, but sooner or later their complexity grows.

Perhaps the project once had only a couple of endpoints running SELECT queries without JOINs. As it grows, however, its mapping layer will eventually need one of the following:

  1. Formatting;

  2. Composing one large object from several smaller ones;

  3. Business logic that affects how data is mapped;

  4. Role-based behavior.

The list goes on. The point is that AutoMapper users tend to put this logic into the mapping configuration because it is the fastest way to get a result when new features affect mapping.

As noted earlier, the library's author has also said that this is wrong. Yet chaos continues to appear in codebases.

Poor Performance

To support this claim, I wrote a simple benchmark. The code is available on GitHub.

I compared AutoMapper with one alternative way to convert objects: an extension method.

public static class UserModelExtensions
{
    public static User ToUser(this UserModel model) =>
        new(model.FirstName, model.LastName, model.BirthDate, model.Address.ToAddress());
}

public static class AddressModelExtensions
{
    public static Address ToAddress(this AddressModel model) =>
        new(model.Latitude, model.Longitude);
}
Enter fullscreen mode Exit fullscreen mode

I tested two cases:

  1. Mapping one object to another;

  2. Mapping a list of 10,000 elements to another list.

The results are shown below:

The M1 is very fast, by the way. On an eighth-generation Intel i5 with Hyper-Threading, AutoMapper was five times slower than the extension method in the List case, compared with three times slower here.

The M1 is very fast, by the way. On an eighth-generation Intel i5 with Hyper-Threading, AutoMapper was five times slower than the extension method in the List case, compared with three times slower here.

Impact on Assembly Size

I do not know how significant this drawback is, but it is still worth mentioning.

I have a pet project in which I removed AutoMapper. The release assembly became almost one megabyte smaller.

Before removal: 1.7 MB

Before removal: 1.7 MB

After removal: ~795 KB

After removal: ~795 KB

When to Use AutoMapper—and When to Map Manually

AutoMapper is a reasonable fit when source and destination shapes are closely related, conventions cover most members, configuration is validated, and the mapping remains at an application boundary. It becomes harder to justify when mappings contain business rules, require frequent debugging, obscure references, or sit on a hot path.

Before adding it, compare the saved assignment code with the ongoing cost of configuration, navigation, runtime failures, and deployment constraints. Explicit constructors, factory methods, extension methods, and source-generated mappers all make different tradeoffs. Choose the least surprising option for the team that will maintain the conversion.

References


Related .NET Architecture Guides

Follow Stepami on GitHub for open-source C#/.NET projects, compiler experiments, production-focused examples, and new releases.

Top comments (0)