I'm using AutoMapper to map my db models with my api models. But I have a problem with custom mapping. I'll try to explain my problem:
So I have the db and api models like this:
public class ApiModel
{
public List<ApiSubModel> Colors { get; set; }
}
public class DbModel
{
public string Type { get; set; }
public string Settings { get; set; }
}
Generally the DbModel Settings property is the serialized version of the ApiModel . So I want to achieve that with a custom convert when creating the maping:
Startup.cs:
services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies())
Mapper profile class:
internal class DbToApiMapping : Profile
{
public DbToApiMapping()
{
CreateMap<ApiModel, DbModel>()
.ConvertUsing((source, dest, context) => new DbModel
{
Type = context.Items["Type"].ToString(),
Settings = JsonConvert.SerializeObject(source)
});
CreateMap<DbModel, ApiModel>()
.ConstructUsing((source, context) =>
{
var res = JsonConvert.DeserializeObject<ApiModel>(source.Settings);
return new ApiModel
{
Colors = res.Colors
};
});
}
}
For the first map I use it like this:
var settings = modelMapper.Map<DbModel>(req.Settings, opt => opt.Items["Type"] = "Setpoint");
For the second map I use it like that:
var ss = modelMapper.Map<ApiModel>(settings.Settings);
The error I get when try to map is as follows:
Message:
AutoMapper.AutoMapperMappingException : Missing type map configuration or unsupported mapping.
Mapping types:
Object -> ApiModel
System.Object -> CommonLibrary.Models.ApiModel
I'm sure that I'm doing something wrong...but I can't quite catch what to look exactly. For the second mapping I tried with .ConvertUsing() method, but the error is the same.
Can someone help with this one.
Thanks in advance
Julian
---EDIT---
As suggested in the comments I tried without the DI. Here is the code:
class Program
{
static void Main(string[] args)
{
var config = new MapperConfiguration( cfg =>
{
cfg.CreateMap<ApiModel, DbModel>()
.ConvertUsing((source, dest, context) => new DbModel
{
Type = context.Items["Type"].ToString(),
Settings = JsonConvert.SerializeObject(source)
});
cfg.CreateMap<DbModel, ApiModel>()
.ConstructUsing((source, context) =>
{
var res = JsonConvert.DeserializeObject<ApiModel>(source.Settings);
return new ApiModel
{
Colors = res.Colors
};
});
});
var modelMapper = config.CreateMapper();
var apiObj = new ApiModel
{
Colors = new List<ApiSubModel>
{
new ApiSubModel
{
SomeProp = "Test",
SubModel = new ApiSubSubModel
{
IntProp = 3,
StringProp = "Alabala"
}
}
}
};
DbModel dbRes = modelMapper.Map<DbModel>(apiObj, opt => opt.Items["Type"] = "Setpoint");
var dbObj = new DbModel
{
Type = "Setpoint",
Settings = "{\"Colors\":[{\"SomeProp\":\"Test\",\"SubModel\":{\"IntProp\":3,\"StringProp\":\"Alabala\"}}]}"
};
var apiRes = modelMapper.Map<ApiModel>(dbObj);
Console.WriteLine(dbRes.Settings);
Console.WriteLine(apiRes.Colors[0].SomeProp);
Console.WriteLine(apiRes.Colors[0].SubModel.StringProp);
Console.ReadLine();
}
}
public class ApiModel
{
public List<ApiSubModel> Colors { get; set; }
}
public class DbModel
{
public string Type { get; set; }
public string Settings { get; set; }
}
public class ApiSubModel
{
public string SomeProp { get; set; }
public ApiSubSubModel SubModel { get; set; }
}
public class ApiSubSubModel
{
public int IntProp { get; set; }
public string StringProp { get; set; }
}
It IS working, but there is something strange, when I want to debug the program and put a break point after var apiRes = modelMapper.Map<ApiModel>(dbObj); and try to debug the value of apiRes it says apiRes error CS0103: The name 'apiRes' does not exist in the current context
I tweaked your code a bit and now it works(although I had to use my own JSON and my own SubApiModel) but you can ask me about it in the comments if you're unsure
My models
public class ApiModel
{
public List<ApiSubModel> Colors { get; set; }
}
public class ApiSubModel
{
public string Name { get; set; }
}
public class DbModel
{
public DbModel(string type, string settings)
{
Type = type;
Settings = settings;
}
public string Type { get; set; }
public string Settings { get; set; }
}
and my JSON
{
Colors:
[
{
"Name": "AMunim"
}
]
}
and this is my mapping configuration:
.CreateMap<DbModel, ApiModel>()
.ConstructUsing((source, context) =>
{
var res = JsonConvert.DeserializeObject<ApiModel>(source.Settings);
return res;
});
This basically deserializes the whole JSON and since it has a property Color which is a list of ApiSubModels, I can simply convert the whole string to Object(of type ApiModel).
This is my complete testing code
using AutoMapper;
using Newtonsoft.Json;
using StackAnswers;
using StackAnswers.Automapper;
using System.Numerics;
DbModel dbModel = new DbModel("very important type", "{Colors: [{\"Name\": \"AMunim\"}]}");
MapperConfiguration config = new(cfg =>
{
cfg.CreateMap<DbModel, ApiModel>()
.ConstructUsing((source, context) =>
{
var res = JsonConvert.DeserializeObject<ApiModel>(source.Settings);
return res;
});
});
Mapper mapper = new(config);
ApiModel apiModel = mapper.Map<DbModel, ApiModel>(dbModel);
Console.WriteLine(apiModel.Colors.Count);
Console.WriteLine(apiModel.Colors.FirstOrDefault()?.Name);
Console.Read();
and the output:
EDIT
You can register your profiles/Mappings individually and force DI to use that i.e. register that
var config = new MapperConfiguration(c => {
//profile
c.AddProfile<DbToApiMapping>();
//mappings
c.CreateMap<DbModel, ApiModel>()
.ConstructUsing((source, context) =>
{
var res = JsonConvert.DeserializeObject<ApiModel>(source.Settings);
return res;
});
});
//now register this instance
services.AddSingleton<IMapper>(s => config.CreateMapper());
Now when you request this service you will get the instance with configuration applied in your ctor
public class BadClass
{
private readonly IMapper _mapper;
BadClass(IMapper mapper)
{
_mapper = mapper;
}
public void HandleFunction()
{
//here you can use this
ApiModel apiModel = _mapper.Map<DbModel, ApiModel>(dbModel);
}
}
I managed to find my mistake, and I have to admit it is a silly one, but took me a lot of time to figure. The problem is in the line var ss = modelMapper.Map<ApiModel>(settings.Settings);
See I have the Profile like this:
CreateMap<DbModel, ApiModel>()
.ConstructUsing((source, context) =>
{
var res = JsonConvert.DeserializeObject<ApiModel>(source.Settings);
return new ApiModel
{
Colors = res.Colors
};
});
It expects the source to be a DbModel object, but in fact I pass a property of this object which is in fact a string. And I do not have defined that kind of mapping, that is why I get the error.
The right usage have to be: var ss = modelMapper.Map<ApiModel>(settings);
So thanks for all your suggestions!
Regards,
Julian
Related
Question:
I have two classes (source and destination) with dto's which inherit from a abstract base class. When I try to map a list with source members to the destination dto's AutoMapper tries to map the missing mapping relation ship to the abstract destination class. How can I avoid this problem?
Information: It's not possible to make sure that all source members have their destination "partner".
These are the source member classes:
namespace Source
{
public abstract class Item
{
public int Id { get; set; }
public int Size { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
public class Asset1 : Item{}
public class Asset2 : Item{}
}
These are the destination member classes:
namespace Destination
{
public abstract class ItemDto
{
public int ProductId { get; set; }
public int Size { get; set; }
public string ProductName { get; set; }
public string Description { get; set; }
}
public class Asset1Dto : ItemDto{}
}
This is the AutoMapper Configuration:
namespace AutoMapper
{
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<Item, ItemDto>(MemberList.Destination)
.Include<Asset1, Asset1Dto>()
.ForMember(o => o.ProductId, ex => ex.MapFrom(o => o.Id))
.ForMember(o => o.ProductName, ex => ex.MapFrom(o => o.Name))
.ReverseMap();
CreateMap<Asset1, Asset1Dto>(MemberList.Destination).ReverseMap();
}
}
}
This is the Main-Class:
namespace AutoMapperExample
{
public class Program
{
private static IList<Item> _items;
private static IMapper _mapper;
private static void Config()
{
_items = new List<Item>();
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile<MappingProfile>();
});
_mapper = new Mapper(config);
}
public static void Main(string[] args)
{
Config();
// Create dummy content
var rnd = new Random().Next();
_items.Add(new Asset1()
{
Name = typeof(Asset1).ToString(),
Id = rnd,
Size = 23,
Description = "Dummy-Text 1"
});
// working case
Console.WriteLine("Working when relation is present:");
try
{
var itemDto = _mapper.Map<IList<ItemDto>>(_items);
var jsonString = JsonSerializer.Serialize(itemDto);
Console.WriteLine(jsonString);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
Console.WriteLine();
rnd = new Random().Next();
_items.Add(new Asset2()
{
Name = typeof(Asset2).ToString(),
Id = rnd,
Size = 23,
Description = "Dummy-Text 2"
});
// case which throws the exception
Console.WriteLine("No destination member present:");
try
{
var itemDto = _mapper.Map<IList<ItemDto>>(_items);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
}
Exception I get:
---> System.ArgumentException: Cannot create an instance of abstract type ItemDto
Here is a gist repo with the example code.
The mapping for Asset2 doesn't work because you didn't configure it anywhere. You just need to add it, the same as with Asset1.
c.CreateMap<Item, ItemDto>()
.Include<Asset1, Asset1Dto>()
.Include<Asset2, Asset1Dto>()
.ForMember(o => o.ProductId, ex => ex.MapFrom(o => o.Id))
.ForMember(o => o.ProductName, ex => ex.MapFrom(o => o.Name))
.ReverseMap();
c.CreateMap<Asset1, Asset1Dto>().ReverseMap();
c.CreateMap<Asset2, Asset1Dto>(MemberList.Destination).ReverseMap();
#lucian-bargaoanu: According to your statement it's not possible to implement AutoMapper in a way to map the source class structure into the destination class structure (see image below) without getting an exception? If this statement is true, how can I solve the problem without adding a "Asst2Dto" class?
Thank you so much for your help!
Desired mapping operation:
var itemList = new List<Item>()
{
new Asset1(),
new Asset2(),
};
var itemDtoList = _mapper.Map<List<ItemDto>>(itemList);
Simplified source and target class diagram:
I have a scenario where I want to create an object (Calc) which takes some options as a constructor argument. It happens I have a serialized version of Calc, its properties and the options properties as another single object. Here's my code:
void Main()
{
var mockMapper = new MapperConfiguration(c =>
{
c.AddProfile<MapperProf>();
})
.CreateMapper();
}
public class MapperProf : Profile
{
public MapperProf()
{
CreateMap<Scen, Calc>()
.ConstructUsing(c => new Calc()) // I AM STUCK HERE
CreateMap<Scen, Opt>()
.ForMember(o => o.OptProp, o => o.MapFrom(o => o.OptProp));
}
}
public class Calc
{
public Calc(Opt opt)
{
OptProp = opt.OptProp;
}
public string CalcProp { get; set; }
private string OptProp { get; set; }
}
public class Opt
{
public string OptProp { get; set; }
}
public class Scen
{
public string CalcProp { get; set; }
public string OptProp { get; set; }
}
For various reasons I cannot access Calc.OptProp, I have to pass it in via the constructor argument.
In equivalent terms what I want to do in one shot is:
Calc c = mockMapper.Map<Calc>().ConstructUsing(c => new Calc(mockMapper.Map<Opt>(scen)));
That is, construct both the Calc and Opt from the same Scen.
In the ConstructUsing you can use ResulotionContext and then use mapper for create constructor like this:
public class MapperProf : Profile
{
public MapperProf()
{
CreateMap<Scen, Opt>().ForMember(o => o.OptProp, o => o.MapFrom(scen => scen.OptProp));
CreateMap<Scen, Calc>().ConstructUsing((scen, context) => new Calc(context.Mapper.Map<Scen, Opt>(scen)));
}
};
And for mapping scen:
var scen = new Scen() { CalcProp = "Calc", OptProp = "Opt" };
var calc = mockMapper.Map<Scen, Calc>(scen);
IncludeMembers() will always map from first match even if object is null.
Let's say we have the following source models:
public class Item
{
public MovieMetadata MovieMetadata { get; set; }
public BookMetadata BookMetadata { get; set; }
}
public class MovieMetadata
{
public string Title { get; set; }
}
public class BookMetadata
{
public string Title { get; set; }
}
Destination model:
public class ItemDetail
{
public string Title { get; set; }
}
Mapping profile:
public class ItemProfile : Profile
{
public ItemProfile()
{
CreateMap<Item, ItemDetail>()
.IncludeMembers(
src => src.BookMetadata,
src => src.MovieMetadata);
CreateMap<BookMetadata, ItemDetail>();
CreateMap<MovieMetadata, ItemDetail>();
}
}
And Program class with instantiation and testing logic:
public class Program
{
public static void Main()
{
//create mapper
var configuration = new MapperConfiguration(cfg =>
{
cfg.AddMaps(typeof(ItemProfile));
});
var mapper = configuration.CreateMapper();
//check if configuration valid
mapper.ConfigurationProvider.AssertConfigurationIsValid();
Console.WriteLine("Mapper configuration is valid");
//try map book metadata
var bookItem = new Item
{
BookMetadata = new BookMetadata()
{
Title = "book"
},
MovieMetadata = null
};
var bookItemDetail = mapper.Map<ItemDetail>(bookItem);
bool isBookCorrectlyMapped = bookItem.BookMetadata.Title == bookItemDetail.Title;
Console.WriteLine($"Book mapped correctly: {isBookCorrectlyMapped}");
//try map movie metadata
var movieItem = new Item
{
BookMetadata = null,
MovieMetadata = new MovieMetadata()
{
Title = "movie"
}
};
var movieItemDetail = mapper.Map<ItemDetail>(movieItem);
bool isMovieCorrectlyMapped = movieItem.MovieMetadata.Title == movieItemDetail.Title;
Console.WriteLine($"Movie mapped correctly: {isMovieCorrectlyMapped}");
}
}
This is the output we will see:
Mapper configuration is valid
Book mapped correctly: True
Movie mapped correctly: False
We see that mapping for item with BookMetadata succeeded, but for MovieMetadata failed. Need to make updates so this test succeeds.
Suppose the reason it's failing is because of order of items in IncludeMembers():
.IncludeMembers(
src => src.BookMetadata,
src => src.MovieMetadata)
Here we have src.BookMetadata first and src.MovieMetadata second. During mapping it will find Title field in src.BookMetadata and will always use this value even if src.BookMetadata is null.
Is there any way to skip src.BookMetadata if it is null and use next src.MovieMetadata instead? Or probably need to use something else instead of IncludeMembers()?
Here is similar issue: https://github.com/AutoMapper/AutoMapper/issues/3204
Code above you can find here to quickly reproduce issue: https://dotnetfiddle.net/9YLGR6
Found possible solutions.
Option 1. IValueResolver
We can create custom value resolver. This solution is good for small amount of fields that we need to map from child objects. As any field will require specific value resolver.
Let's create TitleResolver:
public class TitleResolver : IValueResolver<Item, ItemDetail, string>
{
public string Resolve(Item source, ItemDetail destination, string destMember, ResolutionContext context)
{
if (source != null)
{
if (source.BookMetadata != null)
{
return source.BookMetadata.Title;
}
if (source.MovieMetadata != null)
{
return source.MovieMetadata.Title;
}
}
return null;
}
}
And update ItemProfile:
public class ItemProfile : Profile
{
public ItemProfile()
{
CreateMap<Item, ItemDetail>()
.ForMember(dest => dest.Title, opt => opt.MapFrom<TitleResolver>());
}
}
Link to whole code sample: https://dotnetfiddle.net/6pfKYh
Option 2. BeforeMap()/AfterMap()
In case if our child objects have more than one field to be mapped to destination object it may be a good idea to use BeforeMap() or AfterMap() methods.
In that case ItemProfile will be updated to:
public class ItemProfile : Profile
{
public ItemProfile()
{
CreateMap<Item, ItemDetail>(MemberList.None)
.AfterMap((src, dest, ctx) =>
{
if (src.BookMetadata != null)
{
ctx.Mapper.Map(src.BookMetadata, dest);
}
else if (src.MovieMetadata != null)
{
ctx.Mapper.Map(src.MovieMetadata, dest);
}
});
CreateMap<BookMetadata, ItemDetail>();
CreateMap<MovieMetadata, ItemDetail>();
}
}
Link to whole code sample: https://dotnetfiddle.net/ny1yRU
I'm using an generic method to map two classes using Automapper
My generic methods
public class AutoMapperConfiguration
{
public MapperConfiguration Configure<TSource, TDestination>() where TSource:class where TDestination:class
{
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile<ClientMappingProfile<TSource,TDestination>>();
});
return config;
}
}
ClientMappingProfile.cs
public class ClientMappingProfile<TSource,TDestination>: Profile where TSource : class where TDestination:class
{
public ClientMappingProfile()
{
CreateMap<TSource, TDestination>().ReverseMap();
}
}
StudentDetailsViewModel.cs
public class StudentDetailsViewModel
{
public long ID { get; set; }
public string FirstName { get; set; }
public List<QualificationViewModel> listQualificationViewModel { get; set; }
}
QualificationViewModel.cs
public class QualificationViewModel
{
public long ID { get; set; }
public long StudentID { get; set; }
public string ExaminationPassed { get; set; }
}
StudentValueObject.cs
public class StudentValueObject
{
public long ID { get; set; }
public string FirstName { get; set; }
public List<StudentQualificationValueObject> listStudentQualificationValueObject { get; set; }
}
StudentQualificationValueObject.cs
public class StudentQualificationValueObject
{
public long ID { get; set; }
public long StudentID { get; set; }
public string ExaminationPassed { get; set; }
}
Usage
StudentValueObject studentValueObject = new StudentValueObject();
var config = new AutoMapperConfiguration().Configure<StudentValueObject, StudentDetailsViewModel>();
var iMapper = config.CreateMapper();
studentValueObject = iMapper.Map<StudentDetailsViewModel, StudentValueObject>(objStudentModel);
So, this works fine with Mapping StudentDetailsViewModel.cs with StudentValueObject.cs. But it silently fails to copy my child list objects which is List<QualificationViewModel> to List<StudentQualificationValueObject>. The child list object always seems to be null. I'm pretty newbie to AutoMapper. I need some help as to know where am I going wrong or what need to be added/fixed to my generic method, so that the child list object gets copied to with Parent object.
Update -
Currently I'm doing it using below code and its working properly but I'm confused is this the proper way of doing this.
StudentValueObject studentValueObject = new StudentValueObject();
var config = new AutoMapperConfiguration().Configure<StudentValueObject, StudentDetailsViewModel>();
var iMapper = config.CreateMapper();
studentValueObject = iMapper.Map<StudentDetailsViewModel, StudentValueObject>(objStudentModel);
config = new AutoMapperConfiguration().Configure<StudentQualificationValueObject, QualificationViewModel>();
iMapper = config.CreateMapper();
studentValueObject.listStudentQualificationValueObject = iMapper.Map<List<QualificationViewModel>, List<StudentQualificationValueObject>>(objStudentModel.listQualificationViewModel);
You have to map the list properties, cause they have different names in the given parent types and you have to add a mapping for the types used within both lists. Here is an working example for your code:
public class StudentsMappingProfile : Profile
{
public StudentsMappingProfile()
{
CreateMap<StudentValueObject, StudentDetailsViewModel>()
.ForMember(viewModel => viewModel.listQualificationViewModel, conf => conf.MapFrom(value => value.listStudentQualificationValueObject));
CreateMap<StudentQualificationValueObject, QualificationViewModel>();
}
}
public class Program
{
public static void Main()
{
var config = new MapperConfiguration(cfg => cfg.AddProfile<StudentsMappingProfile>());
var mapper = config.CreateMapper();
var source = new StudentValueObject { ID = 73, FirstName = "Hello", listStudentQualificationValueObject = new List<StudentQualificationValueObject> { new StudentQualificationValueObject { ID = 42, StudentID = 17, ExaminationPassed = "World" } } };
var destination = mapper.Map<StudentDetailsViewModel>(source);
Console.ReadKey();
}
}
I want to use automapper to create absolute url using Automappers profile. What is best practice of doing it?
My profiles are autoconfigured during startup.
I am using an Ioc Container if that might help.
SourceToDestinationProfile : Profile
{
public SourceToDestinationProfile()
{
var map = CreateMap<Source, Destination>();
map.ForMember(dst => dst.MyAbsoluteUrl, opt => opt.MapFrom(src => "http://www.thisiswhatiwant.com/" + src.MyRelativeUrl));
...
}
}
In some way I dynamically want to pick up the request base url ("http://www.thisiswhatiwant.com/") to make it possible to put it together with my relativeurl. I know one way of doing it but it is not pretty, ie can't be the best way.
I don't know whether this is what you are looking for:
public class Source
{
public string Value1 { get; set; }
public string Value2 { get; set; }
}
public class Destination
{
public string Value1 { get; set; }
public string Value2 { get; set; }
}
public class ObjectResolver : IMemberValueResolver<Source, Destination, string, string>
{
public string Resolve(Source s, Destination d, string source, string dest, ResolutionContext context)
{
return (string)context.Items["domainUrl"] + source;
}
}
public class Program
{
public void Main()
{
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Source, Destination>()
.ForMember(o => o.Value1, opt => opt.ResolveUsing<ObjectResolver, string>(m=>m.Value1));
});
var mapper = config.CreateMapper();
Source sr = new Source();
sr.Value1 = "SourceValue1";
Destination de = new Destination();
de.Value1 = "dstvalue1";
mapper.Map(sr, de, opt => opt.Items["domainUrl"] = "http://test.com/");
}
}