How to resolve absolute url with automapper - c#

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/");
}
}

Related

AutoMapper problem with custom convert from source to destination

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

Automapper: Avoid mapping source members without destination member to its abstract base class

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:

Create Object Using Constructor and Argument with AutoMapper

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);

AutoMapper, mapping nested objects of different types

CreateMap<SourceType, DestinationType>()
.ForMember(dest => dest.Type3Property, opt => opt.MapFrom
(
src => new Type3
{
OldValueType5 = src.oldValType6,
NewValueType5 = src.newValType6
}
);
While creating Type3 I have to assign nested properties of Type6 to Type5. How do I do it using Automapper.
We probably still need a more complete example to fully answer the question. But... from what you've given us so far, I think you only need to add a mapping from Type6 to Type5.
Here's an example of mapping those "nested properties". You can copy past this into a console app to run yourself.
class Program
{
static void Main(string[] args)
{
var config = new MapperConfiguration(cfg =>
{
//The map for the outer types
cfg.CreateMap<SourceType, DestinationType>()
.ForMember(dest => dest.Type3Property, opt => opt.MapFrom(src => src.Inner));
//The map for the inner types
cfg.CreateMap<InnerSourceType, Type3>();
//The map for the nested properties in the inner types
cfg.CreateMap<Type6, Type5>()
//You only need to do this if your property names are different
.ForMember(dest => dest.MyType5Value, opt => opt.MapFrom(src => src.MyType6Value));
});
config.AssertConfigurationIsValid();
var mapper = config.CreateMapper();
var source = new SourceType
{
Inner = new InnerSourceType {
OldValue = new Type6 { MyType6Value = 15 },
NewValue = new Type6 { MyType6Value = 20 }
}
};
var result = mapper.Map<SourceType, DestinationType>(source);
}
}
public class SourceType
{
public InnerSourceType Inner { get; set; }
}
public class InnerSourceType
{
public Type6 OldValue { get; set; }
public Type6 NewValue { get; set; }
}
public class DestinationType
{
public Type3 Type3Property { get; set; }
}
//Inner destination
public class Type3
{
public Type5 OldValue { get; set; }
public Type5 NewValue { get; set; }
}
public class Type5
{
public int MyType5Value { get; set; }
}
public class Type6
{
public int MyType6Value { get; set; }
}

Conditionally mapping one source type to two destination types

I have a source DTO like this
public class Member
{
public string MemberId {get;set;}
public string MemberType {get;set;}
public string Name {get;set;}
}
The member type can be "Person" or "Company".
And two destination classes like this
public class PersonMember
{
public int PersonMemberId {get;set;}
public string Name {get;set;}
}
public class CompanyMember
{
public int CompanyMemberId {get;set;}
public string Name {get;set;}
}
I want to use Automapper to check what the value of MemberType is in the source class and depending on that type, map to one of the two destination types.
I saw the example of conditionally mapping, but it maps the field it performs the conditional check on. I want to check the condition and map a different field.
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<Foo,Bar>()
.ForMember(dest => dest.baz, opt => opt.Condition(src => (src.baz >= 0)));
});
My goal is something like this -
cfg.CreateMap<Member, PersonMember>()
.ForMember(dest => PersonMember.PersonMemberId, opt => if the source.MemberType == "Person" perform mapping from MemberId, otherwise do nothing);
cfg.CreateMap<Member, CompanyMember>()
.ForMember(dest => CompanyMember.CompanyMemberId, opt => if the source.MemberType == "Company" perform mapping from MemberId, otherwise do nothing);
Introduce some base class Member. Inherit PersonMember, CompanyMember from the new base class.
Then define these mappings:
cfg.CreateMap<Dto.Member, Member>()
.ConstructUsing((memberDto, context) => {
switch (memberDto.MemberType)
{
case "PersonMember":
return context.Mapper.Map<PersonMember>(memberDto);
case "CompanyMember":
return context.Mapper.Map<CompanyMember>(memberDto);
default:
throw new ArgumentOutOfRangeException($"Unknown MemberType {memberDto.MemberType}");
}
});
cfg.CreateMap<Dto.Member, PersonMember>()
.ForMember(dest => PersonMember.PersonMemberId,
opt => opt.MapFrom(src => src.MemberId));
cfg.CreateMap<Dto.Member, CompanyMember>()
.ForMember(dest => CompanyMember.CompanyMemberId,
opt => opt.MapFrom(src => src.MemberId));
Now you can map using _mapperInstance.Map<Member>(memberDto);
With automapper you must specify return type on invocation mapper eg. mapper.Map<PersonMember>(member), this tells that return type is PersonMember so you can't return CompanyMember.
You can do something like this:
var configPerson = new MapperConfiguration(cfg => cfg.CreateMap<Member, PersonMember>());
var configCompany = new MapperConfiguration(cfg => cfg.CreateMap<Member, CompanyMember>());
PersonMember personMember = null;
CompanyMember companyMember = null;
switch (member.MemberType )
{
case "PersonMember":
var mapper = configPerson.CreateMapper();
personMember = mapper.Map<PersonMember>(member);
break;
case "CompanyMember":
var mapper = configCompany.CreateMapper();
companyMember = mapper.Map<CompanyMember>(member);
break;
default:
throw new Exception("Unknown type");
break;
}
Or you can try Custom type converters with object as return type.
I saw the example of conditionally mapping, but it maps the field it performs the conditional check on. I want to check the condition and map a different field.
Try using such config:
cfg.CreateMap<Member, PersonMember>()
.ForMember(dest => PersonMember.PersonMemberId, opt => {
opt.Condition(src => src.MemberType == "Person");
opt.MapFrom(src => src.MemberId);
});
cfg.CreateMap<Member, CompanyMember>()
.ForMember(dest => CompanyMember.CompanyMemberId, opt => {
opt.Condition(src => src.MemberType == "Company");
opt.MapFrom(src => src.MemberId);
});
In case you mapping from a non-compatible object Id field will be set to 0.
For version 5 and above you could try below code:
using System;
using AutoMapper;
namespace AutoMapOneToMulti
{
class Program
{
static void Main(string[] args)
{
RegisterMaps();
var s = new Source { X = 1, Y = 2 };
Console.WriteLine(s);
Console.WriteLine(Mapper.Map<Source, Destination1>(s));
Console.WriteLine(Mapper.Map<Source, Destination2>(s));
Console.ReadLine();
}
static void RegisterMaps()
{
Mapper.Initialize(cfg => cfg.AddProfile<GeneralProfile>());
}
}
public class GeneralProfile : Profile
{
public GeneralProfile()
{
CreateMap<Source, Destination1>();
CreateMap<Source, Destination2>();
}
}
public class Source
{
public int X { get; set; }
public int Y { get; set; }
public override string ToString()
{
return string.Format("Source = X : {0}, Y : {1}", X, Y);
}
}
public class Destination1
{
public int X { get; set; }
public int Y { get; set; }
public override string ToString()
{
return string.Format("Destination1 = X : {0}, Y : {1}", X, Y);
}
}
public class Destination2
{
public int X { get; set; }
public int Y { get; set; }
public override string ToString()
{
return string.Format("Destination2 = X : {0}, Y : {1}", X, Y);
}
}
}
And for version below 5 you could try this:
using System;
using AutoMapper;
namespace AutoMapOneToMulti
{
class Program
{
static void Main(string[] args)
{
RegisterMaps();
var s = new Source { X = 1, Y = 2 };
Console.WriteLine(s);
Console.WriteLine(Mapper.Map<Source, Destination1>(s));
Console.WriteLine(Mapper.Map<Source, Destination2>(s));
Console.ReadLine();
}
static void RegisterMaps()
{
Mapper.Initialize(cfg => cfg.AddProfile<GeneralProfile>());
}
}
public class GeneralProfile : Profile
{
protected override void Configure()
{
CreateMap<Source, Destination1>();
CreateMap<Source, Destination2>();
}
}
public class Source
{
public int X { get; set; }
public int Y { get; set; }
public override string ToString()
{
return string.Format("Source = X : {0}, Y : {1}", X, Y);
}
}
public class Destination1
{
public int X { get; set; }
public int Y { get; set; }
public override string ToString()
{
return string.Format("Destination1 = X : {0}, Y : {1}", X, Y);
}
}
public class Destination2
{
public int X { get; set; }
public int Y { get; set; }
public override string ToString()
{
return string.Format("Destination2 = X : {0}, Y : {1}", X, Y);
}
}
}
If you want a dynamic function, use this extension:
public static dynamic DaynamicMap(this Source source)
{
if (source.X == 1)
return Mapper.Map<Destination1>(source);
return Mapper.Map<Destination2>(source);
}
Console.WriteLine(new Source { X = 1, Y = 2 }.DaynamicMap());
Console.WriteLine(new Source { X = 2, Y = 2 }.DaynamicMap());

Categories