« Automapper » : différence entre les versions
Apparence
(47 versions intermédiaires par le même utilisateur non affichées) | |||
Ligne 1 : | Ligne 1 : | ||
[[Category: | [[Category:.NET Application]] | ||
= Description = | = Description = | ||
Permet le mapping d'un objet de type A vers un objet de type B.<br /> | Permet le mapping d'un objet de type A vers un objet de type B.<br /> | ||
Ligne 6 : | Ligne 6 : | ||
* [https://github.com/AutoMapper/AutoMapper/wiki/Flattening Flattening] | * [https://github.com/AutoMapper/AutoMapper/wiki/Flattening Flattening] | ||
* Ignore les références null | * Ignore les références null | ||
{{info | [[Mapster]] is a faster alternative.}} | |||
= Liens = | = Liens = | ||
Ligne 24 : | Ligne 25 : | ||
// load the configuration from all classes who inherits from Profile in the current assembly | // load the configuration from all classes who inherits from Profile in the current assembly | ||
cfg. | cfg.AddMaps(typeof(ContainerConfig).Assembly); | ||
}); | }); | ||
Ligne 38 : | Ligne 39 : | ||
public AProfile() | 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[]; | |||
} | } | ||
} | } | ||
Ligne 57 : | Ligne 61 : | ||
== [http://docs.automapper.org/en/stable/Dependency-injection.html Dependency Injection] == | == [http://docs.automapper.org/en/stable/Dependency-injection.html Dependency Injection] == | ||
<filebox fn='Program.cs'> | |||
appBuilder.Services.AddAutoMapper(typeof(Program)); | |||
</filebox> | |||
<filebox fn='Controllers/ItemController.cs'> | |||
private readonly IMapper mapper; | |||
public ItemController(IMapper mapper) | |||
{ | |||
this.mapper = mapper; | |||
} | |||
</filebox> | |||
=== AutoMapper 12- === | |||
{{info | Add the nuget package {{boxx|AutoMapper.Extensions.Microsoft.DependencyInjection}}}} | {{info | Add the nuget package {{boxx|AutoMapper.Extensions.Microsoft.DependencyInjection}}}} | ||
<filebox fn='Startup.cs'> | <filebox fn='Startup.cs'> | ||
public void ConfigureServices(IServiceCollection services) | 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 | // add as parameter one type for each assembly | ||
services.AddAutoMapper(typeof(ProfileTypeFromAssembly1), typeof(ProfileTypeFromAssembly2)); | services.AddAutoMapper(typeof(ProfileTypeFromAssembly1), typeof(ProfileTypeFromAssembly2)); | ||
</filebox> | |||
<filebox fn='Controllers/ItemController.cs'> | |||
private readonly IMapper mapper; | |||
public ItemController(ItemDbContext context, IMapper mapper) | |||
{ | |||
this.mapper = mapper; | |||
} | |||
</filebox> | |||
=== WPF === | |||
<filebox fn='App.xaml.cs'> | |||
protected override void RegisterTypes(IContainerRegistry containerRegistry) | |||
{ | |||
var config = new MapperConfiguration(cfg => | |||
{ | |||
cfg.AddMaps(typeof(App).Assembly); | |||
}); | |||
containerRegistry.RegisterSingleton<IMapper>(() => config.CreateMapper()); | |||
} | |||
</filebox> | </filebox> | ||
Ligne 100 : | Ligne 143 : | ||
</kode> | </kode> | ||
* [http://docs.automapper.org/en/stable/Flattening.html Flattening] | * [http://docs.automapper.org/en/stable/Flattening.html Flattening] | ||
{{info | If the mapping has not been configured, you will get an {{boxx|AutoMapper.AutoMapperMappingException}}}} | {{info | If the mapping to a destination property has not been configured, you will get an {{boxx|AutoMapper.AutoMapperMappingException}}}} | ||
== List and polymorphism | == Record == | ||
For records with a primary constructor, use {{boxx|ForCtorParam}} and fill all the constructor parameters. | |||
<kode lang='cs'> | |||
CreateMap<A, B>() | |||
.ForCtorParam(nameof(B.P1), opt => opt.MapFrom(src => src.P1)) | |||
.ForCtorParam(nameof(B.Q2), opt => opt.MapFrom(src => src.P2)) | |||
.ForCtorParam(nameof(B.C1P1), opt => opt.MapFrom(src => src.C1.P1)); | |||
}); | |||
record A(int P1, int P2, C C1); | |||
record B(int P1, int Q2, int C1P1); | |||
record C(int P1); | |||
</kode> | |||
= [https://docs.automapper.org/en/stable/Flattening.html Flattening] = | |||
<kode lang='cs'> | |||
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; } | |||
} | |||
</kode> | |||
= Unflattening = | |||
<kode lang='cs'> | |||
// unflattening UserDto.GroupId → User.Group.Id | |||
this.CreateMap<UserDto, User>() | |||
.ForPath(dest => dest.Group.Id, opt => opt.MapFrom(src => src.GroupId)); | |||
</kode> | |||
= List and polymorphism = | |||
<kode lang='cs'> | <kode lang='cs'> | ||
// avec une liste | // avec une liste | ||
Ligne 142 : | Ligne 280 : | ||
* [http://docs.automapper.org/en/stable/Mapping-inheritance.html Mapping Inheritance] | * [http://docs.automapper.org/en/stable/Mapping-inheritance.html Mapping Inheritance] | ||
= [http://codebuckets.com/2016/09/24/passing-parameters-with-automapper/ Pass parameter to the mapping] = | |||
<kode lang='cs'> | <kode lang='cs'> | ||
var config = new MapperConfiguration(cfg => | var config = new MapperConfiguration(cfg => | ||
Ligne 148 : | Ligne 286 : | ||
cfg.CreateMap<A, B>() | cfg.CreateMap<A, B>() | ||
.ForMember(dest => dest.P1, | .ForMember(dest => dest.P1, | ||
opt => opt.MapFrom((src, | opt => opt.MapFrom((src, _, _, context) => src.P1 + (int)context.Items["+"])); | ||
}); | }); | ||
Ligne 155 : | Ligne 293 : | ||
var a = new A { P1 = 1 }; | var a = new A { P1 = 1 }; | ||
var b = mapper.Map<B>(a, opt => opt.Items["+"] = 9); | var b = mapper.Map<B>(a, opt => opt.Items["+"] = 9); | ||
</kode> | |||
= Map 1 object to multiple objects = | |||
<kode lang='cs'> | |||
// 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; } | |||
} | |||
</kode> | |||
== Flatten to ValueType == | |||
<kode lang='cs' collapsed> | |||
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(); | |||
</kode> | |||
== Flatten with a ITypeConverter == | |||
<kode lang='cs' collapsed> | |||
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; | |||
} | |||
} | |||
} | |||
</kode> | |||
= [https://docs.automapper.org/en/stable/Conditional-mapping.html#preconditions Conditional mapping] = | |||
<kode lang='cs'> | |||
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); | |||
}); | |||
</kode> | |||
= [https://docs.automapper.org/en/stable/Before-and-after-map-actions.html Before and After Map Action] = | |||
<kode lang='cs'> | |||
cfg.CreateMap<A, B>() | |||
.BeforeMap((src, dest) => src.P1 += 1) | |||
.AfterMap((src, dest) => dest.P1 -= 1); | |||
</kode> | |||
= Inject a dependency during the mapping with a ValueResolver = | |||
<filebox fn='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)); | |||
</filebox> | |||
<filebox fn='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(); | |||
} | |||
} | |||
</filebox> | |||
= Errors = | |||
== [https://stackoverflow.com/questions/32643076/automapper-complex-if-else-statement-in-formember/32643134 A lambda expression with a statement body cannot be converted to an expression tree] == | |||
<kode lang='cs'> | |||
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) => | |||
{ | |||
// ... | |||
})); | |||
</kode> | </kode> | ||
Ligne 162 : | Ligne 449 : | ||
mapperMock.Setup(x => x.Map<ItemDto>(It.IsAny<Item>())) | mapperMock.Setup(x => x.Map<ItemDto>(It.IsAny<Item>())) | ||
.Returns(new ItemDto()); | .Returns(new ItemDto()); | ||
</kode> | |||
= [https://docs.automapper.org/en/stable/Configuration-validation.html Unit tests] = | |||
<filebox fn='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); | |||
} | |||
</filebox> | |||
== Dependency Injection == | |||
The ctor of {{boxx|MyValueResolver}} take a parameter of type {{boxx|IMyService}}.<br> | |||
So to test the profile, you have to inject {{boxx|IMyService}} to resolve {{boxx|MyValueResolver}}. | |||
<filebox fn='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>(); | |||
} | |||
</filebox> | |||
== Mock the mapper == | |||
<kode lang='cs'> | |||
// 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()); | |||
})); | |||
</kode> | </kode> | ||
Ligne 172 : | Ligne 538 : | ||
public void ConfigureServices(IServiceCollection services) | public void ConfigureServices(IServiceCollection services) | ||
{ | { | ||
services.AddAutoMapper(); | services.AddAutoMapper(typeof(Startup)); | ||
</filebox> | </filebox> | ||
<filebox fn='MyController.cs'> | <filebox fn='MyController.cs'> | ||
private readonly IMapper | private readonly IMapper mapper; | ||
public MyController(IMapper mapper) | public MyController(IMapper mapper) | ||
{ | { | ||
this.mapper = mapper; | |||
B b = | B b = this.mapper.Map<A, B>(a); | ||
</filebox> | </filebox> | ||
Configuration dans Profile | Configuration dans Profile |
Dernière version du 4 mars 2025 à 15:17
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
![]() |
Mapster is a faster alternative. |
Liens
- Getting Started
- Static and Instance API
- AutoMapper.Collection bibliothèque externe pour géré les collections
- Map 1 object to list of objects
Setup and configuration
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);
|
Profile
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[];
}
}
|
Dependency Injection
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 |
Record
For records with a primary constructor, use ForCtorParam and fill all the constructor parameters.
CreateMap<A, B>()
.ForCtorParam(nameof(B.P1), opt => opt.MapFrom(src => src.P1))
.ForCtorParam(nameof(B.Q2), opt => opt.MapFrom(src => src.P2))
.ForCtorParam(nameof(B.C1P1), opt => opt.MapFrom(src => src.C1.P1));
});
record A(int P1, int P2, C C1);
record B(int P1, int Q2, int C1P1);
record C(int P1);
|
Flattening
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; }
}
|
Pass parameter to the mapping
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
|
Flatten with a ITypeConverter
|
Conditional mapping
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);
});
|
Before and After Map Action
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
A lambda expression with a statement body cannot be converted to an expression tree
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) =>
{
// ...
}));
|
MOQ
var mapperMock = new Mock<IMapper>();
mapperMock.Setup(x => x.Map<ItemDto>(It.IsAny<Item>()))
.Returns(new ItemDto());
|
Unit tests
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);
}
|
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>();
}
|
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());
}));
|
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