Description
Permet le mapping d'un objet de type A vers un objet de type B.
AutoMapper utilise des convention de mapping afin de réduire le paramétrage:
- Propriétés de même nom
- Flattening
- Ignore les références null
Liens
|
var config = new MapperConfiguration(cfg => {
// configure mapping from Source to Dest
cfg.CreateMap<Source, Dest>();
// load the configuration from AppProfile
cfg.AddProfile<AppProfile>();
// load the configuration from all classes who inherits from Profile in the current assembly
cfg.AddMaps(typeof(ContainerConfig).Assembly);
});
IMapper mapper = config.CreateMapper();
IMapper mapper = new Mapper(config);
|
AProfile.cs
|
internal sealed class AProfile : Profile
{
public AProfile()
{
CreateMap<A, B>()
.ForMember(dest => dest.B1, opt => opt.MapFrom(src => src.A1));
CreateMap<B, A>()
.ForMember(dest => dest.A1, opt => opt.MapFrom(src => src.B1));
.ForAllOtherMembers(opt => opt.Ignore()); // ignore all other members
CreateMap<A, C[]>()
.ConvertUsing<AToCsConverter>();
}
}
public class AToCsConverter : ITypeConverter<A, C[]>
{
public C[] Convert(A source, C[] destination, ResolutionContext context)
{
return new C[];
}
}
|
Program.cs
|
appBuilder.Services.AddAutoMapper(typeof(Program));
|
Controllers/ItemController.cs
|
private readonly IMapper mapper;
public ItemController(IMapper mapper)
{
this.mapper = mapper;
}
|
AutoMapper 12-
|
Add the nuget package AutoMapper.Extensions.Microsoft.DependencyInjection |
Startup.cs
|
public void ConfigureServices(IServiceCollection services)
{
// if all the profiles are in the current assembly
services.AddAutoMapper(typeof(Startup));
// add as parameter one type for each assembly
services.AddAutoMapper(typeof(ProfileTypeFromAssembly1), typeof(ProfileTypeFromAssembly2));
|
Controllers/ItemController.cs
|
private readonly IMapper mapper;
public ItemController(ItemDbContext context, IMapper mapper)
{
this.mapper = mapper;
}
|
WPF
App.xaml.cs
|
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
var config = new MapperConfiguration(cfg =>
{
cfg.AddMaps(typeof(App).Assembly);
});
containerRegistry.RegisterSingleton<IMapper>(() => config.CreateMapper());
}
|
Exemple
|
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<A, B>()
.ForMember(dst => dst.Q2, opts => opts.MapFrom(src => src.P2)); // map A.P2 → B.Q2
.ReverseMap(); // map B → A
});
IMapper mapper = config.CreateMapper();
C c = new C() { P1 = 3 };
A a = new A() { P1 = 1, C1 = c };
B b = mapper.Map<B>(a);
// b.P1 = 1, b.Q2 = 2, b.C1P1 = 3
class A
{
public int P1 { get; set; }
public int P2 { get; set; }
public C C1 { get; set; }
}
class B
{
public int P1 { get; set; } // mapping par convention avec A.P1
public int Q2 { get; set; } // mapping par configuration avec A.P2
public int C1P1 { get; set; } // mapping par flattening avec A.C1.P1
}
class C
{
public int P1 { get; set; }
}
|
|
If the mapping to a destination property has not been configured, you will get an AutoMapper.AutoMapperMappingException |
|
var config = new MapperConfiguration(cfg =>
{
// default flattening
// naming convention: SpecificItem.Category → SpecificItemDto.Category
// flatening conventing: SpecificItem.Item.Id → SpecificItemDto.ItemId
// configuration: SpecificItem.Item.Code → SpecificItemDto.Code
cfg.CreateMap<SpecificItem, SpecificItemDto>()
.ForMember(dest => dest.Code, opt => opt.MapFrom(src => src.Item.Code));
// flattening from composition to inheritance
// naming convention: SpecificItem.Category → SpecificItemDtoV2.Category
// configuration: explicitly call the mapping of Item → SpecificItemDtoV2 for the Item property
cfg.CreateMap<SpecificItem, SpecificItemDtoV2>()
.IncludeMembers(x => x.Item);
// naming convention: Item.Id → ItemDtoV2.Id
// naming convention: Item.Code → ItemDtoV2.Code
cfg.CreateMap<Item, ItemDtoV2>();
// MemberList.None to skip validation and ignore unmapped destination properties like Category
cfg.CreateMap<Item, SpecificItemDtoV2>(MemberList.None)
.IncludeBase<Item, ItemDtoV2>();
});
config.AssertConfigurationIsValid();
IMapper mapper = config.CreateMapper();
var specificItem = new SpecificItem
{
Item = new Item
{
Id = 1,
Code = 2
},
Category = 3
};
var specificItemDto = mapper.Map<SpecificItemDto>(specificItem);
var specificItemDtoV2 = mapper.Map<SpecificItemDtoV2>(specificItem);
// source
class Item
{
public int Id { get; set; }
public int Code { get; set; }
}
class SpecificItem
{
public int Category { get; set; }
public Item Item { get; set; }
}
// destination
class SpecificItemDto
{
public int ItemId { get; set; }
public int Code { get; set; }
public int Category { get; set; }
}
class ItemDtoV2
{
public int Id { get; set; }
public int Code { get; set; }
}
class SpecificItemDtoV2 : ItemDtoV2
{
public int Category { get; set; }
}
|
Unflattening
|
// unflattening UserDto.GroupId → User.Group.Id
this.CreateMap<UserDto, User>()
.ForPath(dest => dest.Group.Id, opt => opt.MapFrom(src => src.GroupId));
|
List and polymorphism
|
// avec une liste
List<B> listB = Mapper.Map<List<A>, List<B>>(listA);
// polymorphism
cfg.CreateMap<Parent1, Parent2>()
.Include<Child1, Child2>();
// IncludeAllDerived(); to include all the derived classes
// without the include, the resulting list contains only Parent2 objects (no Child2 objects)
cfg.CreateMap<Child1, Child2>();
var lp1 = new List<Parent1> { new Parent1 { P1 = 1}, new Child1 { P1 = 2, P2 = 22 } };
var lp2 = mapper.Map<List<Parent2>>(lp1);
// Parent2, Child2
class Parent1
{
public int P1 { get; set; }
}
class Child1 : Parent1
{
public int P2 { get; set; }
}
class Parent2
{
public int P1 { get; set; }
}
class Child2 : Parent2
{
public int P2 { get; set; }
}
|
|
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<A, B>()
.ForMember(dest => dest.P1,
opt => opt.MapFrom((src, _, _, context) => src.P1 + (int)context.Items["+"]));
});
IMapper mapper = config.CreateMapper();
var a = new A { P1 = 1 };
var b = mapper.Map<B>(a, opt => opt.Items["+"] = 9);
|
Map 1 object to multiple objects
|
// map 1 Group to many User
class Group
{
public int Id { get; set; }
public ICollection<int> UserIds { get; } = new List<int>();
}
class User
{
public int Id { get; set; }
public int GroupId { get; set; }
}
|
Flatten to ValueType
|
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<(Group group, int userId), User>()
.IncludeMembers(s => s.group) // call the mapping Group → User
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.userId));
cfg.CreateMap<Group, User>()
.ForMember(dest => dest.GroupId, opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.Id, opt => opt.Ignore());
});
IMapper mapper = config.CreateMapper();
var group = new Group
{
Id = 9,
UserIds = {1, 2, 3}
};
IEnumerable<(int groupId, int userId)> groupAndUserList = group.UserIds.Select(x => ValueTuple.Create(group.Id, x));
var users = mapper.Map<IEnumerable<User>>(groupAndUserList).ToList();
|
Flatten with a ITypeConverter
|
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<int, User>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src));
cfg.CreateMap<Group, User>()
.ForMember(dest => dest.GroupId, opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.Id, opt => opt.Ignore());
cfg.CreateMap<Group, IEnumerable<User>>()
.ConvertUsing<GroupToUsersConverter>();
});
IMapper mapper = config.CreateMapper();
var group = new Group
{
Id = 9,
UserIds = { 1, 2, 3 }
};
var users = mapper.Map<IEnumerable<User>>(group).ToList();
// use a converter to flatten a Group
class GroupToUsersConverter : ITypeConverter<Group, IEnumerable<User>>
{
public IEnumerable<User> Convert(
Group source,
IEnumerable<User> destination,
ResolutionContext context)
{
foreach (var userId in source.UserIds)
{
var user = context.Mapper.Map<User>(userId);
context.Mapper.Map(source, user);
yield return user;
}
}
}
|
|
cfg.CreateMap<A, B>()
// map A.P2 → B.Q2 if A.P2 > 0
// if A.P2 <= 0 → B.Q2 = 0
.ForMember(dest => dest.Q2,
opt =>
{
opt.Condition(src => src.P2 > 0);
opt.MapFrom(src => src.P2);
});
|
|
cfg.CreateMap<A, B>()
.BeforeMap((src, dest) => src.P1 += 1)
.AfterMap((src, dest) => dest.P1 -= 1);
|
Inject a dependency during the mapping with a ValueResolver
Program.cs
|
// UserProvider is injected as a dependency, and AutoMapper has to be injected as a dependency too
appBuilder.Services.AddScoped<IUserProvider, UserProvider>()
.AddAutoMapper(typeof(AProfile));
|
AProfile.cs
|
public class AProfile : Profile
{
public AProfile()
{
this.CreateMap<A, B>()
.ForMember(x => x.User, opt => opt.MapFrom<UserValueResolver>());
}
}
// UserProvider is retrieved as a dependency and used to resolve the mapping
public class UserValueResolver(IUserProvider userProvider) : IValueResolver<A, B, User>
{
public User Resolve(A source, B destination, User member, ResolutionContext context)
{
return userProvider.GetUser();
}
}
|
Errors
|
this.CreateMap<Class1, Class2>()
.ForMember(dest => dest.Prop1, opt => opt.MapFrom(src =>
{
// ...
}));
// use a new overload of MapFrom
this.CreateMap<Class1, Class2>()
.ForMember(dest => dest.Prop1, opt => opt.MapFrom((src, dest) =>
{
// ...
}));
|
|
var mapperMock = new Mock<IMapper>();
mapperMock.Setup(x => x.Map<ItemDto>(It.IsAny<Item>()))
.Returns(new ItemDto());
|
MyProfileTests.cs
|
public class MyProfileTests
{
private readonly Fixture fixture;
private readonly MapperConfiguration configuration;
private readonly IMapper mapper;
public MyProfileTests()
{
fixture = new();
configuration = new MapperConfiguration(cfg => cfg.AddProfile<MyProfile>());
mapper = configuration.CreateMapper();
}
[Fact]
public void MyProfile_IsValid()
=> configuration.AssertConfigurationIsValid();
[Fact]
public void Map_SourceToDestination_ShouldMapCorrectly()
{
// Arrange
var source = fixture.Create<MySource>();
// Act
var destination = mapper.Map<MyDestination>(source);
// Assert
Assert.NotNull(destination);
Assert.Equal(source.Property1, destination.Property1);
}
|
Mock the mapper
|
// create a mock mapper
var mockMapper = new Mock<IMapper>();
// mock the mapping
this.mockMapper.Setup(x => x.Map<MyDto>(It.IsAny<MyEntity>()))
.Returns(myDto);
// or create a new simplified mapper
var mapper = new Mapper(
new MapperConfiguration(
cfg =>
{
cfg.CreateMap<MyEntity, MyDto>()
.ForMember(dest => dest.Property1, opt => opt.Ignore());
}));
|
Dependency Injection
The ctor of MyValueResolver take a parameter of type IMyService.
So to test the profile, you have to inject IMyService to resolve MyValueResolver.
MyProfileTests.cs
|
public class MyProfileTests
{
private readonly Fixture fixture;
private readonly IMapper mapper;
public MyProfileTests()
{
mockMyService = new Mock<IMyService>();
mockApplicationUserProvider
.Setup(x => x.GetData())
.Returns(fixture.Create<Data>());
var serviceProvider = new ServiceCollection()
.AddSingleton(mockMyService.Object)
.AddSingleton<MyValueResolver>()
.AddAutoMapper(cfg => cfg.AddProfile<MyProfile>())
.BuildServiceProvider();
mapper = serviceProvider.GetRequiredService<IMapper>();
}
|
Installation
NuGet → AutoMapper
ASP.NET Core
NuGet → AutoMapper.Extensions.Microsoft.DependencyInjection
Startup.cs
|
public void ConfigureServices(IServiceCollection services)
{
services.AddAutoMapper(typeof(Startup));
|
MyController.cs
|
private readonly IMapper mapper;
public MyController(IMapper mapper)
{
this.mapper = mapper;
B b = this.mapper.Map<A, B>(a);
|
Configuration dans Profile