Introduction
AutoMapper is an essential .NET library for automating mappings between DTOs, entities, and view models, eliminating tedious boilerplate code. In 2026, with .NET 10, it's optimized for performance and natively supports EF Core projections. Why use it? Imagine mapping a complex UserEntity to a lightweight UserDto in just one line: that's 80% less code, fewer errors, and faster refactoring. This intermediate tutorial starts with the basics and progresses to advanced configurations like profiles, custom resolvers, and validations. You'll end up with a functional console project, easily extensible to ASP.NET Core, featuring practical e-commerce examples for orders and products. Result: a robust, testable, high-performance mapper ready to use.
Prerequisites
- .NET 10 SDK installed (check with
dotnet --version) - Visual Studio 2026 or VS Code with C# extension
- Basic C# knowledge (classes, LINQ, DI)
- A code editor and terminal
Create the Project and Install AutoMapper
dotnet new console -n AutoMapperDemo
cd AutoMapperDemo
dotnet add package AutoMapper
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Hosting.Abstractions
code .These commands create a .NET 10 console project, install the core AutoMapper package, its DI extension, and Hosting/DI packages for a modern setup. The DI extension simplifies injecting IMapper everywhere. Avoid outdated versions: always use the latest for JIT and AOT optimizations.
Define Source and Target Models
Before mapping, let's define concrete classes. We'll use an e-commerce scenario: OrderEntity (from the DB) to OrderDto (for the API). The entity includes internal fields like an auto-generated Id and a product list; the DTO exposes only public info with a calculated total.
Complete Model Classes
namespace AutoMapperDemo;
public class ProductEntity
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}
public class ProductDto
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}
public class OrderEntity
{
public int Id { get; set; }
public string CustomerName { get; set; } = string.Empty;
public DateTime OrderDate { get; set; }
public List<ProductEntity> Products { get; set; } = new();
public decimal Discount { get; set; }
}
public class OrderDto
{
public int Id { get; set; }
public string CustomerName { get; set; } = string.Empty;
public DateTime OrderDate { get; set; }
public List<ProductDto> Products { get; set; } = new();
public decimal Total => Products.Sum(p => p.Price) - Discount;
}These classes show a typical mapping: the entity sources a Discount, while the DTO derives a Total. Note the nested collections – AutoMapper handles them recursively. Pitfall: always initialize lists to avoid runtime NullRef exceptions.
Basic Mapper Configuration
Analogy: The mapper acts like an automatic translator that matches properties by name and convention. Here, we set up a simple mapping that ignores Id if needed and handles collections.
Basic IMapper Configuration
using AutoMapper;
namespace AutoMapperDemo;
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<ProductEntity, ProductDto>();
CreateMap<OrderEntity, OrderDto>()
.ForMember(dest => dest.Id, opt => opt.Ignore());
}
}
public static class MapperConfig
{
public static MapperConfiguration CreateConfiguration()
{
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile<MappingProfile>();
});
return config;
}
}We use a Profile to organize mappings (best practice). CreateMap auto-maps by name; ForMember ignores Id to simulate a DTO without a primary key. This config is thread-safe and reusable. Pitfall: without Ignore(), AutoMapper would overwrite the client-side ID.
Integrate with Dependency Injection
For professional use, inject IMapper via DI. It's scalable for ASP.NET Core APIs. We'll host the app with IHost for testing.
DI Setup and Initial Mapping
using AutoMapper;
using AutoMapperDemo;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
class Program
{
static async Task Main(string[] args)
{
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) =>
{
var config = MapperConfig.CreateConfiguration();
services.AddSingleton(config.CreateMapper());
services.AddSingleton<IMapper>(sp => config.CreateMapper());
})
.Build();
var mapper = host.Services.GetRequiredService<IMapper>();
var service = new MappingService(mapper);
await service.RunDemo();
}
}
public class MappingService
{
private readonly IMapper _mapper;
public MappingService(IMapper mapper) => _mapper = mapper;
public async Task RunDemo()
{
var orderEntity = new OrderEntity
{
Id = 1,
CustomerName = "Alice",
OrderDate = DateTime.Now,
Products = new List<ProductEntity>
{
new() { Name = "Laptop", Price = 1200m },
new() { Name = "Souris", Price = 25m }
},
Discount = 50m
};
var orderDto = _mapper.Map<OrderDto>(orderEntity);
Console.WriteLine($"Total: {orderDto.Total}");
await Task.CompletedTask;
}
}DI with AddSingleton for performance (mappers are stateless). The MappingService demos a live mapping: entity → DTO with calculated total (1225 - 50 = 1175). Run dotnet run to confirm output. Pitfall: avoid calling CreateMapper() twice – use a single instance.
Advanced Mappings with Conditions
Level up to intermediate features: conditions (Condition) and custom resolvers for business logic.
Custom Resolver and Conditions
using AutoMapper;
namespace AutoMapperDemo;
public class CustomTotalResolver : IValueResolver<OrderEntity, OrderDto, decimal>
{
public decimal Resolve(OrderEntity source, OrderDto destination, decimal destMember, ResolutionContext context)
{
var total = source.Products.Sum(p => p.Price);
return total > 1000 ? total * 0.9m : total - source.Discount;
}
}
public class AdvancedMappingProfile : Profile
{
public AdvancedMappingProfile()
{
CreateMap<ProductEntity, ProductDto>();
CreateMap<OrderEntity, OrderDto>()
.ForMember(dest => dest.Total, opt => opt.MapFrom<CustomTotalResolver>())
.ForPath(dest => dest.Products.First().Name, opt => opt.Condition((src, dest, name) => !string.IsNullOrEmpty(name)));
}
}The custom IValueResolver applies a 10% promo if total >1000 (business logic). Condition skips if name is empty. Add this profile to MapperConfig with cfg.AddProfile. Update Program.cs: total becomes 1080 (10% off). Pitfall: resolvers must be thread-safe – keep them stateless.
Validation and Unit Tests
Best practice: Always validate mappings at startup to catch errors early.
Validation and Unit Test
using AutoMapper;
using FluentAssertions;
using Xunit;
namespace AutoMapperDemo.Tests;
public class MapperTests
{
[Fact]
public void ShouldMapOrderCorrectly()
{
// Arrange
var config = MapperConfig.CreateConfiguration();
config.AssertConfigurationIsValid(); // Validation
var mapper = config.CreateMapper();
var entity = new OrderEntity { /* data as before */ };
// Act
var dto = mapper.Map<OrderDto>(entity);
// Assert
dto.CustomerName.Should().Be(entity.CustomerName);
dto.Products.Should().HaveCount(2);
}
}AssertConfigurationIsValid() throws if mappings are incomplete (e.g., missing properties). Integrate with Xunit tests (dotnet add package xunit). Use FluentAssertions for readable asserts. Pitfall: without validation, runtime errors occur on unmapped properties.
Best Practices
- Always use Profiles: Separate mappings by domain (OrdersProfile, UsersProfile) for maintainability.
- Inject IMapper via DI: Singleton for performance, scoped in ASP.NET for DB context.
- Validate at startup: Call
mapper.ConfigurationProvider.AssertConfigurationIsValid()inProgram.cs. - Prefer ProjectTo for EF: Use
context.Orders.ProjectToinstead of(mapper.ConfigurationProvider) ToList().Map(). - Ignore sensitive IDs:
ForMember(dest => dest.Id, opt => opt.Ignore())for security.
Common Errors to Avoid
- Null or uninjected Mapper: Ensure
services.AddAutoMapper(typeof(Program))with the DI extension. - Performance on large collections: Use EF
ProjectToinstead ofToList().Map()(avoids N+1). - DTO over-posting: Map only necessary properties; validate with
ForAllMembers(opt => opt.Condition(...)). - Poor null handling: Add
AllowNullCollections = trueor initialize lists in DTOs.
Next Steps
- Official docs: AutoMapper.org
- Integrate with EF Core: Read about
ProjectTofor optimized queries. - Full project source: Adapt to ASP.NET Core for APIs.
- Check out our advanced .NET trainings at Learni to master MediatR + AutoMapper in Clean Architecture.