Skip to content
Learni
View all tutorials
.NET

How to Configure AutoMapper in .NET in 2026

Lire en français

Introduction

AutoMapper is a mature .NET library that automates object mapping, eliminating tedious manual boilerplate. In 2026, with .NET 10 and its AOT optimizations, it's still essential for REST APIs, microservices, and Blazor apps, reducing code by 70% on average per official benchmarks.

Why use it? Imagine mapping a DTO to an entity: without AutoMapper, 20 lines of repetitive code; with it, one fluent line. This intermediate tutorial covers full setup in a console project that's extensible to ASP.NET Core, from basic mappings to custom resolvers and dependency injection. By the end, you'll master performance tweaks and avoid common pitfalls for scalable apps. Ready to boost your productivity? (128 words)

Prerequisites

  • .NET SDK 10.0 or higher (check with dotnet --version)
  • Editor like Visual Studio 2022 or VS Code with C# extension
  • Basic C# knowledge (classes, LINQ) and DI in .NET
  • Terminal (PowerShell or bash)

Create the Console Project

terminal-init.sh
dotnet new console -o AutoMapperDemo --framework net10.0
cd AutoMapperDemo
code .

This command creates a minimal .NET 10 console project, navigates into it, and opens VS Code. It lays the foundation for testing AutoMapper without unnecessary complexity, targeting the 2026 LTS framework.

Install AutoMapper

Add the essential NuGet packages. AutoMapper core handles mappings; Extensions.Microsoft.DependencyInjection integrates with .NET's native DI for smooth production setups.

Add the NuGet Packages

terminal-packages.sh
dotnet add package AutoMapper
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
dotnet add package AutoMapper.DependencyInjection
dotnet restore

These packages install AutoMapper (5.0+ in 2026) with DI support. restore syncs everything; stick to stable versions for AOT compatibility.

Define Source and Destination Models

Models.cs
namespace AutoMapperDemo;

public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
    public DateTime BirthDate { get; set; }
    public List<string> Hobbies { get; set; } = new();
}

public class UserDto
{
    public int Id { get; set; }
    public string FullName { get; set; } = string.Empty;
    public int Age { get; set; }
    public string[] Hobbies { get; set; } = Array.Empty<string>();
}

These classes represent a User entity (DB source) and UserDto (API). Note the misaligned properties: FullName and Age will need a custom profile to avoid mapping errors.

Create the Mapping Profile

Profiles centralize configuration like a blueprint. They extend Profile and use CreateMap for declarative rules, making code testable and maintainable.

Implement MappingProfile

MappingProfile.cs
using AutoMapper;

namespace AutoMapperDemo;

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<User, UserDto>()
            .ForMember(dest => dest.FullName, opt => opt.MapFrom(src => src.FirstName + " " + src.LastName))
            .ForMember(dest => dest.Age, opt => opt.MapFrom(src => DateTime.Now.Year - src.BirthDate.Year))
            .ForMember(dest => dest.Hobbies, opt => opt.MapFrom(src => src.Hobbies.ToArray()));
    }
}

This profile maps User to UserDto with ForMember for custom properties: concatenation for FullName, approximate age calculation (refine in production), and List-to-array conversion. Rule order matters for dependencies.

Set Up DI and Mapping

Integrate AutoMapper via IServiceCollection as a singleton service. Use MapperConfiguration to validate at startup and prevent runtime errors.

Update Program.cs with DI

Program.cs
using AutoMapper;
using AutoMapper.Extensions.Microsoft.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace AutoMapperDemo;

var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddAutoMapper(typeof(MappingProfile));

builder.Services.AddSingleton(provider =>
{
    var config = new MapperConfiguration(cfg => cfg.AddProfile<MappingProfile>());
    config.AssertConfigurationIsValid();
    return config.CreateMapper();
});

var host = builder.Build();

var mapper = host.Services.GetRequiredService<IMapper>();

var user = new User
{
    Id = 1,
    FirstName = "Jean",
    LastName = "Dupont",
    BirthDate = new DateTime(1990, 5, 15),
    Hobbies = new List<string> { "Lecture", "Natation" }
};

var dto = mapper.Map<UserDto>(user);

Console.WriteLine($"FullName: {dto.FullName}, Age: {dto.Age}, Hobbies: {string.Join(", ", dto.Hobbies)}");

await host.RunAsync();

This modern Program.cs (.NET 10 top-level statements) sets up DI with AddAutoMapper, validates config via AssertConfigurationIsValid(), and tests the mapping. Output: "Jean Dupont, 36, Lecture, Natation". Easily scalable to web APIs.

Advanced Mapping with Custom Resolver

CustomResolverProfile.cs
using AutoMapper;

namespace AutoMapperDemo;

public class AgeResolver : IValueResolver<User, UserDto, int>
{
    public int Resolve(User source, UserDto destination, int destMember, ResolutionContext context)
    {
        var age = DateTime.Now.Year - source.BirthDate.Year;
        if (DateTime.Now < source.BirthDate.AddYears(age)) age--;
        return age;
    }
}

public class AdvancedMappingProfile : Profile
{
    public AdvancedMappingProfile()
    {
        CreateMap<User, UserDto>()
            .ForMember(dest => dest.FullName, opt => opt.MapFrom(src => src.FirstName + " " + src.LastName))
            .ForMember(dest => dest.Age, opt => opt.MapFrom<AgeResolver>())
            .ForMember(dest => dest.Hobbies, opt => opt.Ignore()); // Ignore for testing
    }
}

Introduces a precise IValueResolver for age (handles birthdays). Ignore() skips Hobbies. Swap in Program.cs with AddProfile() for better accuracy, perfect for complex business logic.

Bidirectional Mapping and Collections

BidirectionalProfile.cs
using AutoMapper;

namespace AutoMapperDemo;

public class BidirectionalProfile : Profile
{
    public BidirectionalProfile()
    {
        CreateMap<User, UserDto>().ReverseMap();
        CreateMap<List<User>, List<UserDto>>().ReverseMap();
    }
}

// Example usage in Program.cs:
// var users = new List<User> { user1, user2 };
// var dtos = mapper.Map<List<UserDto>>(users);
// var usersBack = mapper.Map<List<User>>(dtos);

ReverseMap() enables reverse mapping in one call. For collections, map List explicitly. Pitfall: non-assignable properties (e.g., computed ones) need DoNotValidate() or custom resolvers.

Best Practices

  • Always validate: Use AssertConfigurationIsValid() at startup to catch errors early.
  • Profiles per feature: One Profile per bounded context (e.g., UsersProfile, OrdersProfile) for scalability.
  • DI everywhere: Inject IMapper via constructor, never new Mapper() for thread-safety.
  • Performance: Use ProjectTo() with EF Core (avoids materialization); profile with MapperConfiguration.AllowAdditiveProfileAssembly.
  • Testing: Mock IMapper with Substitute.For() (NSubstitute) for unit tests.

Common Errors to Avoid

  • Missing DI: new MapperConfiguration() without services leads to unshared singletons and memory leaks.
  • Circular mappings: Without DisableCircularReferenceHandling(), you get stack overflows; use ConstructUsingServiceLocator().
  • Nullable properties: Set AllowNullCollections = true or ForMember(..., opt => opt.Condition(src => src != null)).
  • AOT/trimming: For .NET native, add false or [DynamicallyAccessedMembers] on resolvers.

Next Steps