Mapping
Mapping
When an API talks to the outside world, it shouldn't leak its domain internals. Mapping gives us a clean boundary:
- Encapsulation & safety – Expose only what clients need (DTOs), hide domain-only fields, and avoid over-posting attacks.
- Stable contracts – Evolve domain models without breaking clients; version DTOs independently.
- Clear invariants – Create/modify domain objects via explicit logic; don't let raw HTTP payloads shape your core.
- Consistency & DRY – Centralize transformation rules instead of hand-writing the same conversions in controllers.
- Testability – Map rules live in one place and can be unit-tested.
How we use AutoMapper (in our architecture)
- Put mapping profiles in the
Applicationlayer (it seesDomain+Contracts). - Use
AutoMapperprimarily for Domain → Contracts (read models / responses). - The
Use Caseclasses inject theIMapperinterface
Implementation
- Install the
AutoMapperand theMicrosoft.Extensions.Configurationnuget package in theapplicationlibrary - Get a license key for AutoMapper and add it to your
appsettings.Development.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"BricksForKidsDBConnectionString": "Connection string"
},
"AutoMapperLicenseKey": "Your key comes here"
}
- Create a
DependencyInjectionclass (similar to the one in ourinfrastructurelibrary)
namespace SweDemoBackend.Application
{
public static class DependencyInjection
{
public static IServiceCollection AddApplication(this IServiceCollection services, IConfiguration configuration)
{
// Scans the Application assembly for Profile classes
services.AddAutoMapper(cfg => cfg.LicenseKey = configuration["AutoMapperLicenseKey"], Assembly.GetExecutingAssembly());
// Add use cases
services.AddScoped<LegoSetUseCase>();
return services;
}
}
}
- Call this extension method in the
Program.csof the API.
Mapping profiles
Instead of manually creating our DTOs from our Entities we want to create Mapping Profiles. No longer this:
namespace SweDemoBackend.Application.UseCases
{
public class LegoSetUseCase
{
private readonly ILegoSetRepository _repo;
public LegoSetUseCase(ILegoSetRepository repo)
{
_repo = repo;
}
public async Task<IEnumerable<LegoSetDto>> GetAllLegoSets(CancellationToken ct = default)
{
var entities = await _repo.GetLegoSetsAsync(ct);
//Mapping
List<LegoSetDto> dtos = new List<LegoSetDto>();
entities.ToList().ForEach(e =>
dtos.Add(new LegoSetDto()
{
Id = e.Id,
Name = e.Name,
NumberOfPieces = e.NumberOfPieces,
}));
return dtos;
}
}
}
Create a LegoSetProfile class in the application library and extend from the Profile class (from AutoMapper):
namespace SweDemoBackend.Application.MappingProfiles
{
public class LegoSetProfile : Profile
{
public LegoSetProfile()
{
CreateMap<LegoSet, LegoSetDto>();
}
}
}
The actual mapping is done by injecting the IMapper interface in each UseCase class where mapping is needed:
namespace SweDemoBackend.Application.UseCases
{
public class LegoSetUseCase
{
private readonly ILegoSetRepository _repo;
private readonly IMapper _mapper;
public LegoSetUseCase(ILegoSetRepository repo, IMapper mapper)
{
_repo = repo;
_mapper = mapper;
}
public async Task<IEnumerable<LegoSetDto>> GetAllLegoSets(CancellationToken ct = default)
{
var entities = await _repo.GetLegoSetsAsync(ct);
//Mapping
var dtos = _mapper.Map<List<LegoSetDto>>(entities);
return dtos;
}
}
}
How are the mapping profiles found?
The highlighted line of code registers AutoMapper and scans the Application assembly for all classes that derive from AutoMapper.Profile, auto-loading their mappings so you don't have to register each profile manually.
public static IServiceCollection AddApplication(this IServiceCollection services, IConfiguration configuration)
{
// Scans the Application assembly for Profile classes
services.AddAutoMapper(cfg => cfg.LicenseKey = configuration["AutoMapperLicenseKey"], Assembly.GetExecutingAssembly());
// Add use cases
services.AddScoped<LegoSetUseCase>();
return services;
}
AutoMapper functionalities/patterns
AutoMapper shines when you need more than 1:1 property copies. Below are the most common AutoMapper use cases.
Custom member mapping (ForMember,MapFrom)
Great for shaping Domain → DTO reads, flattening nested objects, or converting value objects.
CreateMap<Workshop, WorkshopDto>()
.ForMember(d => d.Id, o => o.MapFrom(s => s.WorkshopId))
.ForMember(d => d.CapacityLabel, o => o.MapFrom(s =>
s.Capacity >= 100 ? "Large" : "Standard"));
- By default properties with the same name and type are copied by convention
.ForMember(d => d.Id, o => o.MapFrom(s => s.WorkshopId)): Overrides the default mapping forId: we now map theWorkshopIdinto the DTO'sId.ForMember(d => d.CapacityLabel, o => o.MapFrom(s => s.Capacity >= 100 ? "Large" : "Standard"));: Creates a computed field on the DTO, based on theCapacityproperty from the entity
Create opposite mapping with ReverseMap()
When you define a map …
CreateMap<Workshop, WorkshopDto>();
… calling .ReverseMap() tells AutoMapper to automatically create the opposite mapping too (Destination → Source). The reverse map inherits your configuration and conventions, so you don’t have to write two separate maps.
Why it’s useful
- Cuts boilerplate when conversions are symmetrical (e.g., edit forms, admin tools, import/export).
- You can still customize the reverse side right after calling
.ReverseMap().
CreateMap<Workshop, WorkshopDto>()
.ForMember(d => d.Id, o => o.MapFrom(s => s.WorkshopId))
.ForMember(d => d.CapacityLabel, o => o.MapFrom(s =>
s.Capacity >= 100 ? "Large" : "Standard"))
.ReverseMap()
.ForMember(s => s.CreatedAt,o => o.Ignore());