« Automapper » : différence entre les versions

De Banane Atomic
Aller à la navigationAller à la recherche
 
(19 versions intermédiaires par le même utilisateur non affichées)
Ligne 1 : Ligne 1 :
[[Category:CSharp]]
[[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.AddProfiles(typeof(ContainerConfig).Assembly);
     cfg.AddMaps(typeof(ContainerConfig).Assembly);
});
});


Ligne 38 : Ligne 39 :
     public AProfile()
     public AProfile()
     {
     {
         this.CreateAToBMapping();
         CreateMap<A, B>()
         this.CreateBToAMapping();
            .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


    private void CreateAToBMapping()
         CreateMap<A, C[]>()
    {
             .ConvertUsing<AToCsConverter>();
         this.CreateMap<A, B>()
             .ForMember(dest => dest.B1, opt => opt.MapFrom(src => src.A1));
     }
     }
}


     private void CreateBToAMapping()
public class AToCsConverter : ITypeConverter<A, C[]>
{
     public C[] Convert(A source, C[] destination, ResolutionContext context)
     {
     {
         this.CreateMap<B, A>()
         return new C[];
            .ForMember(dest => dest.A1, opt => opt.MapFrom(src => src.B1));
            .ForAllOtherMembers(opt => opt.Ignore()); // ignore all other members
     }
     }
}
}
Ligne 58 : 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 102 : Ligne 144 :
* [http://docs.automapper.org/en/stable/Flattening.html Flattening]
* [http://docs.automapper.org/en/stable/Flattening.html Flattening]
{{info | If the mapping to a destination property 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}}}}
= [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 =
= Unflattening =
Ligne 156 : Ligne 272 :
     cfg.CreateMap<A, B>()
     cfg.CreateMap<A, B>()
         .ForMember(dest => dest.P1,
         .ForMember(dest => dest.P1,
             opt => opt.MapFrom((src, dest, destMember, resContext) => src.P1 + (int)resContext.Items["+"]));
             opt => opt.MapFrom((src, _, _, context) => src.P1 + (int)context.Items["+"]));
});
});


Ligne 263 : Ligne 379 :
</kode>
</kode>


= Before mapping =
= [https://docs.automapper.org/en/stable/Before-and-after-map-actions.html Before and After Map Action] =
<kode lang='cs'>
<kode lang='cs'>
cfg.CreateMap<A, B>()
cfg.CreateMap<A, B>()
   .BeforeMap((src, dest) => src.P1 = src.P1 + 1);
   .BeforeMap((src, dest) => src.P1 += 1)
  .AfterMap((src, dest) => dest.P1 -= 1);
</kode>
</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 =
= Errors =
Ligne 306 : Ligne 450 :


Assert.Equal(item.Property1, mappedItem.Property1);
Assert.Equal(item.Property1, mappedItem.Property1);
</kode>
== Mock ==
<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>



Dernière version du 12 septembre 2024 à 12:12

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

Setup and configuration

Cs.svg
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

Cs.svg
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

Flattening

Cs.svg
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

Cs.svg
// unflattening UserDto.GroupId → User.Group.Id
this.CreateMap<UserDto, User>()
    .ForPath(dest => dest.Group.Id, opt => opt.MapFrom(src => src.GroupId));

List and polymorphism

Cs.svg
// 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

Cs.svg
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

Cs.svg
// 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

Cs.svg
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

Cs.svg
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;
        }
    }
}

Conditional mapping

Cs.svg
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

Cs.svg
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

Cs.svg
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

Cs.svg
var mapperMock = new Mock<IMapper>();
mapperMock.Setup(x => x.Map<ItemDto>(It.IsAny<Item>()))
          .Returns(new ItemDto());

Unit tests

Cs.svg
var configuration = new MapperConfiguration(cfg => cfg.AddProfile<ItemProfile>());
configuration.AssertConfigurationIsValid();

var mapper = configuration.CreateMapper();

var item = new Item { /* ... */ };

var itemDto = mapper.Map<ItemDto>(item);
var mappedItem = mapper.Map<Item>(itemDto);

Assert.Equal(item.Property1, mappedItem.Property1);

Mock

Cs.svg
// 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