I have a .Net 5 Web Api project and want to use
Mapster v7.2.0
to avoid mapping objects manually. The following code shows a sample scenario
setup a mapping configuration
map from multiple sources
map to fields with different names
.
[ApiController]
[Route("[controller]")]
public class MyController : ControllerBase
{
[HttpGet]
public ActionResult<UsernameWithTodoTitle> Get()
{
TypeAdapterConfig<(User, Todo), UsernameWithTodoTitle>
.NewConfig()
.Map(dest => dest, src => src.Item1) // map everything from user
.Map(dest => dest, src => src.Item2) // map everything from todo
.Map(dest => dest.TodoTitle, src => src.Item2.Title); // map the special fields from todo
var user = new User { Username = "foo", FieldFromUser = "x" };
var todo = new Todo { Title = "bar", FieldFromTodo = "y" };
var usernameWithTodoTitle = (user, todo).Adapt<(User, Todo), UsernameWithTodoTitle>();
return Ok(usernameWithTodoTitle);
}
}
public class User
{
public string Username { get; set; }
public string FieldFromUser { get; set; }
}
public class Todo
{
public string Title { get; set; } // !! map this one to the TodoTitle field !!
public string FieldFromTodo { get; set; }
}
public class UsernameWithTodoTitle
{
public string Username { get; set; }
public string TodoTitle { get; set; } // !! this one is special, is has a different name !!
public string FieldFromUser { get; set; }
public string FieldFromTodo { get; set; }
}
When running the app the mapping seems to work fine this way
I had to setup the configuration this way, other ways didn't work for me. But there are 3 things left to be solved
The configuration looks wrong to me. It maps everything from the todo and maps the special field again ... so it might loop through multiple times? This might get expensive, if there are multiple fields with different names
I created the configuration inside the controller. How can I create a reusable mapping profile class registered once globally?
When having a mapping profile this line var usernameWithTodoTitle = (user, todo).Adapt<(User, Todo), UsernameWithTodoTitle>(); looks quite messy to me. Better would be var usernameWithTodoTitle = UsernameWithTodoTitle.Adapt((user, todo)) /* pass in as a tuple */ because based on the parameter type it chooses the correct mapping profile
Do you guys have any ideas how to create such a mapping profile?
Updated: Couldn't find way to do what you are trying to do with Mapster, but here is an example of it working with Automapper.
using AutoMapper;
using System;
namespace ConsoleApp5
{
class A { public string FirstName { get; set; } }
public class B { public string Address1 { get; set; } }
public class C
{
public string FirstName { get; set; }
public string Address1 { get; set; }
}
public class DemoProfile : Profile
{
public DemoProfile()
{
CreateMap<(A, B), C>()
.ForMember(dest=> dest.FirstName, opts => opts.MapFrom(src => src.Item1.FirstName))
.ForMember(dest => dest.Address1, opts => opts.MapFrom(src => src.Item2.Address1));
}
}
class Program
{
static void Main(string[] args)
{
var config = new MapperConfiguration(cfg => {
cfg.AddProfile<DemoProfile>();
});
var mapper = config.CreateMapper();
var destination = mapper.Map<C>((new A { FirstName = "Test" }, new B { Address1 = "Addr" }));
Console.ReadKey();
}
}
}
Hey I haven't used Mapster before till now but here is what I gather. It is very specific about the type of tuple you use Tuple<T1,T2> over (T1,T2) but aside from that minor thing I was able to get it running and mapping without issues. Here is a small console example as example.
using Mapster;
using System;
namespace ConsoleApp5
{
class A { public string FirstName { get; set; } }
public class B { public string Address1 { get; set; } }
public class C
{
public string FirstName { get; set; }
public string Address1 { get; set; }
}
class Program
{
static void Main(string[] args)
{
// Option 1
TypeAdapterConfig<Tuple<A, B>, C>.NewConfig()
.Map(dest => dest.FirstName, src => src.Item1.FirstName)
.Map(dest => dest.Address1, src => src.Item2.Address1);
var destObject = new Tuple<A, B>(new A { FirstName = "Test" }, new B { Address1 = "Address 1" })
.Adapt<Tuple<A, B>, C>();
// Option 2
TypeAdapterConfig<(A, B), C>.NewConfig()
.Map(dest => dest.FirstName, src => src.Item1.FirstName)
.Map(dest => dest.Address1, src => src.Item2.Address1);
var destObject2 = (new A { FirstName = "Test" }, new B { Address1 = "Address 1" })
.Adapt<(A, B), C>();
Console.ReadKey();
}
}
}
I managed to do it with Mapster. What I did was
in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Some other magical code
// Tell Mapster to scan this assambly searching for the Mapster.IRegister
// classes and execute them
TypeAdapterConfig.GlobalSettings.Scan(Assembly.GetExecutingAssembly());
}
Create another class like this
using Mapster;
namespace Your.Cool.Namespace
{
public class MappingConfig : IRegister
{
public void Register(TypeAdapterConfig config)
{
// Put your mapping logic here
config
.NewConfig<MySourceType, MyDestinyType>()
.Map(dest => dest.PropA, src => src.PropB);
}
}
}
The key part is using TypeAdapterConfig.GlobalSettings, which is a static public singleton used by Mapster to hold the mappig config. If you do what Jack suggests, it will be a complety new TypeAdapterConfig and not the actual one being used by Mapster and won't work (at least it didn't for me).
On your unit tests remember to load the mapping profile too
[AssemblyInitialize] // Magic part 1 ~(˘▾˘~)
public static void AssemblyInitialization(TestContext testContext)
{
// Magic part 2 (~˘▾˘)~
TypeAdapterConfig.GlobalSettings.Scan(AppDomain.CurrentDomain.GetAssemblies());
}
You can use next:
var config = new TypeAdapterConfig()
{
RequireExplicitMapping = true,
RequireDestinationMemberSource = true,
Compiler = exp => exp.CompileFast()
};
config.Scan("Your assembly");
services.AddSingleton(config);
services.AddTransient<IMapper, ServiceMapper>();
public class RegisterConfig : IRegister
{
public void Register(TypeAdapterConfig config)
{
config.NewConfig<TSource, TDestination>();
}
}
Where services is IServiceCollection
Based on #Felipe Ramos answer I wasn't able to solve it with Mapster but with Automapper. This is my solution just for the sake of completeness. Please let me know if there is a solution for Mapster!
I installed the packages
AutoMapper v10.1.1
AutoMapper.Extensions.Microsoft.DependencyInjection v8.1.1
Inside the method Startup.ConfigureServices I added the line services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
The whole code then looks like
[ApiController]
[Route("[controller]")]
public class MyController : ControllerBase
{
private readonly IMapper _mapper;
public MyController(IMapper mapper)
{
_mapper = mapper;
}
[HttpGet]
public ActionResult<UsernameWithTodoTitle> Get()
{
var user = new User { Username = "foo", FieldFromUser = "x" };
var todo = new Todo { Title = "bar", FieldFromTodo = "y" };
var usernameWithTodoTitle = _mapper.Map<UsernameWithTodoTitle>((user, todo));
return Ok(usernameWithTodoTitle);
}
}
public class User
{
public string Username { get; set; }
public string FieldFromUser { get; set; }
}
public class Todo
{
public string Title { get; set; } // !! map this one to the TodoTitle field !!
public string FieldFromTodo { get; set; }
}
public class UsernameWithTodoTitle
{
public string Username { get; set; }
public string TodoTitle { get; set; } // !! this one is special, is has a different name !!
public string FieldFromUser { get; set; }
public string FieldFromTodo { get; set; }
}
public class UsernameWithTodoTitleMappingProfile : Profile
{
public UsernameWithTodoTitleMappingProfile()
{
CreateMap<(User, Todo), UsernameWithTodoTitle>()
.ForMember(
destination => destination.Username,
memberOptions => memberOptions.MapFrom(source => source.Item1.Username))
.ForMember(
destination => destination.TodoTitle,
memberOptions => memberOptions.MapFrom(source => source.Item2.Title))
.ForMember(
destination => destination.FieldFromUser,
memberOptions => memberOptions.MapFrom(source => source.Item1.FieldFromUser))
.ForMember(
destination => destination.FieldFromTodo,
memberOptions => memberOptions.MapFrom(source => source.Item2.FieldFromTodo));
}
}
Related
I'm new in AutoMapper and I have problem with mapping my generic class to generic dto (using AutoMapper). I have this class:
public class Document<T>
{
public string Id { get; set; }
public Tag[] Tags { get; set; }
public T? Data { get; set; }
}
and this dto:
public class DocumentDto<T>
{
public string Id { get; set; }
public string[] Tags { get; set; }
public T? Data { get; set; }
}
and I need to make two-way mapping.. I created mapper profile in which I define this mapping like this:
public MapperProfile()
{
CreateMap<Document<FinancialReport>, DocumentDto<FinancialReportDto>>()
.ForMember(docDto => docDto.Tags, opt => opt.MapFrom(doc => doc.Tags.Select(tag => tag.Value).ToArray()));
CreateMap<DocumentDto<FinancialReportDto>, Document<FinancialReport>>()
.ForMember(docDto => docDto.Tags, opt => opt.MapFrom(doc => doc.Tags.Select(tag => new Tag { Value = tag }).ToArray()));
}
And then I setting this profile in extension method for dependency injection for IMapper:
public static IServiceCollection AddAutoMapper(this IServiceCollection services)
{
services.AddSingleton<IMapper>(sp =>
{
var config = new MapperConfiguration(cfg => {
cfg.AddProfile<MapperProfile>();
});
return config.CreateMapper();
});
return services;
}
And after this all when I try remap from DocumentDto=>Document or vice versa I got error: AutoMapperMappingException: Missing type map configuration or unsupported mapping.
I tryed googled for hours but nothing helped... Any ideas what I'm doing wrong?
UPDATE 1:
I'm calling mapping like this:
public async Task<IResponse> UpdateFinancialReport(DocumentDto<FinancialReportDto> documentDto)
{
var doc = _mapper.Map<Document<FinancialReport>>(documentDto);
}
ok, I found the problem.. In MapperProfile I was missing mapping config for generic types:
CreateMap<FinancialReport, FinancialReportDto>()
.ReverseMap();
When field names don't match, Mapster won't map. Fields are set to nulls or zeroes depending on type.
How do I use custom names for target attributes?
Ex.: How do I change SchoolClassTeacherName to just Name? Changing it in the code below and in class SchoolDTO gives me null as a result of mapping.
I've read the documentation but there is no answer. Please, help me. Thank you.
config.NewConfig<SchoolPoco, SchoolDTO>()
.Map(dest => dest.SchoolClassTeacherName,
src => src.School.Class.Teacher.Name)
I have this in my Startup.cs
var typeAdapterConfig = TypeAdapterConfig.GlobalSettings;
typeAdapterConfig.Scan(Assembly.GetExecutingAssembly());
var mapperConfig = new Mapper(typeAdapterConfig);
services.AddSingleton<IMapper>(mapperConfig);
I have mapping in a class
public class ProjectMappingProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
// Mappings here
}
}
Use TypeAdapterConfig as described in documentation.
Make sure that School, Class, Teacher, Name properties is set.
Sample code:
using Mapster;
TypeAdapterConfig<DtoA, DtoB>
.NewConfig()
.Map(dest => dest.Main_Property, src => src.MainProperty)
.Map(dest => dest.Inner_Property, src => src.InnerDto.InnerProperty);
DtoA a = new DtoA()
{
MainProperty = "Hello",
InnerDto = new InnerDto() { InnerProperty = "World" }
};
DtoB b = a.Adapt<DtoB>();
class DtoA
{
public string MainProperty { get; set; }
public InnerDto InnerDto { get; set; }
}
class InnerDto
{
public string InnerProperty { get; set; }
}
class DtoB
{
public string Main_Property { get; set; }
public string Inner_Property { get; set; }
}
Result in b variable:
I got a very strange problem,when i add a property named [IdTypeCode] with decimal type in
Destination,the Map method will throw an exception. Does anyone know how to solve it? Thanks.
public class Source
{
public int Id { get; set; }
}
public class Destination
{
public int Id { get; set; }
public decimal IdTypeCode { get; set; }
}
class Program
{
static void Main(string[] args)
{
var configuration = new MapperConfiguration(c => c.CreateMap<Source, Destination>());
var mapper = configuration.CreateMapper();
//will throw an exception
var dest = mapper.Map<Destination>(new Source
{
Id = 1
});
}
}
I believe this is a side-effect of AutoMapper attempting to "flatten" the source Id property into the destination object.
Since your destination has the property IdTypeCode, AutoMapper will try to find a matching value from the source. And in this case, it seems to be picking up on the Type.GetTypeCode() method on the Source.Id property, and hence trying to map a System.TypeCode to a decimal. This can be seen in the exception that's thrown:
Destination Member: IdTypeCode
---> AutoMapper.AutoMapperMappingException: Missing type map configuration or unsupported mapping.
Mapping types:
TypeCode -> Decimal
System.TypeCode -> System.Decimal
This can be verified by changing the type of Destination.IdTypeCode to System.TypeCode:
public class Source
{
public int Id { get; set; }
}
public class Destination
{
public int Id { get; set; }
public TypeCode IdTypeCode { get; set; }
}
class Program
{
static void Main(string[] args)
{
var configuration = new MapperConfiguration(c => c.CreateMap<Source, Destination>());
var mapper = configuration.CreateMapper();
//will throw an exception
var dest = mapper.Map<Destination>(new Source
{
Id = 1
});
Console.WriteLine(dest.IdTypeCode);
}
}
which causes the mapping to succeed and the Destination.IdTypeCode getting mapped with the value Int32.
With that said, aside from changing the name of the property, one easy solution is just to ignore that property on the destination:
var configuration = new MapperConfiguration(c =>
{
c.CreateMap<Source, Destination>()
.ForMember(d => d.IdTypeCode, opt => opt.Ignore());
});
according to Lucian's answer, i change my code as below,it works well
public class Source
{
public string Card { get; set; }
}
public class Destination
{
public string Card { get; set; }
public decimal CardTypeCode { get; set; }
}
class Program
{
static void Main(string[] args)
{
var configuration = new MapperConfiguration(configure =>
{
//set this func
configure.ShouldMapMethod = _ => false;
configure.CreateMap<Source, Destination>();
});
var mapper = configuration.CreateMapper();
var dest = mapper.Map<Destination>(new Source
{
Card = "1"
});
}
}
I am trying to simplify a process of mapping classes. For this purpose I am using AutoMapper. If no one of classes does not have a nested collection then all is great. But if someone have, I have a trouble with mapping. Collection into a class is overwriting. I found an extension AutoMapper.Collection. But it does not work for me.
I am working on a web api project, so some parts of codes here:
// Classes for mapping
public class A_Dto
{
public int Id { get; set; }
public List<A_SubItem_Dto> SubItems { get; set; }
}
public class A_SubItem_Dto
{
public int Id { get; set; }
public string Value { get; set; }
public A_Dto A_MainObject { get; set; }
}
public class A_Resource
{
public int Id { get; set; }
public List<A_SubItem_Resource> SubItems { get; set; }
}
public class A_SubItem_Resource
{
public int Id { get; set; }
public string Value { get; set; }
}
// Configure AutoMapper
public void ConfigureServices(IServiceCollection services)
{
services.AddAutoMapper(cfg => cfg.AddCollectionMappers(), typeof(Startup));
}
// Configure profile class for mapping
public class AutoMapping : Profile
{
CreateMap<A_Dto, A_Resource>().ReverseMap()
.ForMember(m => m.Id, opt => opt.Ignore());
CreateMap<A_SubItem_Dto, A_SubItem_Resource>().ReverseMap()
.EqualityComparison((sir, si) => sir.Id == si.Id);
}
// Controller action with simple test
public async Task<IActionResult> Put(/*...*/)
{
// Test
var aDto = new A_Dto()
{
Id = 7,
SubItems = new List<A_SubItem_Dto>()
{
new A_SubItem_Dto() { Id = 2, Value = "_2_" },
new A_SubItem_Dto() { Id = 9, Value = "_9_" }
}
};
var aResource = new A_Resource()
{
Id = 7,
SubItems = new List<A_SubItem_Resource>()
{
new A_SubItem_Resource() { Id = 2, Value = "_222222_" }
}
};
_mapper.Map(aResource, aDto);
}
In result aDto object contains new collection with one element of aResource.SubItems
Could you tell me please, what I am doing wrong?
I'm trying to use Automapper to map to objects, the issue is one of the objects I'm trying to map has a prefix 'Cust_' in front of all its properties and one doesn't. Is there a way to make this mapping.
For example say I have
class A
{
String FirstName { get; set; }
String LastName { get; set; }
}
class B
{
String Cust_FirstName { get; set; }
String Cust_LastName { get; set; }
}
Obviously this map won't work
AutoMapper.Mapper.CreateMap<A, B>();
b = AutoMapper.Mapper.Map<A, B>(a);
Mapper.Initialize(cfg =>
{
cfg.RecognizeDestinationPrefixes("Cust_");
cfg.CreateMap<A, B>();
});
A a = new A() {FirstName = "Cliff", LastName = "Mayson"};
B b = Mapper.Map<A, B>(a);
//b.Cust_FirstName is "Cliff"
//b.Cust_LastName is "Mayson"
Or alternatively:
Mapper.Configuration.RecognizeDestinationPrefixes("Cust_");
Mapper.CreateMap<A, B>();
...
B b = Mapper.Map<A, B>(a);
...
The documentation has an article on Recognizing pre/postfixes
Sometimes your source/destination properties will have common pre/postfixes that cause you to have to do a bunch of custom member mappings because the names don't match up. To address this, you can recognize pre/postfixes:
public class Source {
public int frmValue { get; set; }
public int frmValue2 { get; set; }
}
public class Dest {
public int Value { get; set; }
public int Value2 { get; set; }
}
Mapper.Initialize(cfg => {
cfg.RecognizePrefix("frm");
cfg.CreateMap<Source, Dest>();
});
Mapper.AssertConfigurationIsValid();
By default AutoMapper recognizes the prefix "Get", if you need to clear the prefix:
Mapper.Initialize(cfg => {
cfg.ClearPrefixes();
cfg.RecognizePrefixes("tmp");
});