How to resolve source member for multi-property mapping in AutoMapper? - c#

I have DO and DTO classes:
public class DO
{
public int Property1 { get; set; }
public int Property2 { get; set; }
}
public class DTO
{
public int MappedProperty1 { get; set; }
public int MappedProperty2 { get; set; }
}
The mapping registration for the above classes are like below.
// From DO to DTO:
configuration.CreateMap<DO, DTO>()
.ForMember(d => d.MappedProperty1, o => o.MapFrom(s => s.Property1))
.ForMember(d => d.MappedProperty2, o => o.MapFrom(s => s.Property1 + s.Property2));
// From DTO to DO:
configuration.CreateMap<DTO, DO>()
.ForMember(d => d.Property1, o => o.MapFrom(s => s.MappedProperty1))
.ForMember(d => d.Property2, o => o.MapFrom(s => s.MappedProperty1 - s.MappedProperty2));
Elsewhere in the code I need to get the destination property names in the mapping. I'm currently doing this by:
var map= mapper.ConfigurationProvider.FindTypeMapFor<DTO, DO>();
var property = map.GetPropertyMaps().First(f => f.SourceMember == typeof(DTO).GetProperty("MappedProperty1"));
var name = property.DestinationProperty.Name;
This works fine with MappedProperty1. However, it does not work with MappedProperty2 as there are two properties participating in the mapping. Instead, I get the error message:
"Sequence contains no matching element".
I also tried with the following instead of First filter and in debug I can see that the count in the list is zero.
var map= mapper.ConfigurationProvider.FindTypeMapFor<DTO, DO>();
var property = map.GetPropertyMaps().Where(w => w.SourceMember == typeof(DTO).GetProperty("MappedProperty2")).ToList();
How can I achieve this source name resolution for multi-property mapping?

Related

How to map a collection inside a object with automapper

I don't know how to map 'Poses' which is inside my dto coming into my controller.
My DTO looks like this
public class NewInstructorProgramDto
{
public ICollection<int> Poses { get; set; }
}
I'm have an object that looks like this below where I need to map my DTO to this
public class InstructorProgram : BaseEntity
{
public ICollection<InstructorProgramPose> Poses { get; set; }
}
where InstructorProgramPose looks like this
public class InstructorProgramPose : BaseEntity
{
public int PoseId { get; set; }
public Pose Pose { get; set; }
public int InstructorProgramId { get; set; }
public InstructorProgram InstructorProgram { get; set; }
}
In my controller I map it like this
var newInstructorProgram = _mapper.Map<NewInstructorProgramDto, InstructorProgram>(newInstructorProgramDto);
and in my mapping file I need to figure out how to map all the integers for Poses to a collection of 'InstructorProgramPose' objects, where the integer is the PoseId
Here is what I have so far but this won't work because it only maps a single InstructorProgramPose, not a collection
CreateMap<NewInstructorProgramDto, InstructorProgram>()
.ForMember(d => d.Poses, o => o.MapFrom(s => new InstructorProgramPose() { PoseId = s.}));
I think I'm on the right track with this code below
CreateMap<NewInstructorProgramDto, InstructorProgram>()
.ForMember(d => d.Poses, o => o.MapFrom(s => s.Poses));
CreateMap<ICollection<int>, ICollection<InstructorProgramPose>>()
.ForMember(d => d.Select(i => i.PoseId), o => o.MapFrom(s => s.Select(i => i)));
but the debugger is throwing an error that says
Exception has occurred: CLR/AutoMapper.AutoMapperConfigurationException
An exception of type 'AutoMapper.AutoMapperConfigurationException' occurred in AutoMapper.dll but was not handled in user code: 'Custom configuration for members is only supported for top-level individual members on a type.'
UPDATE - Just to give you an idea of what I'm trying to do in Automapper, I've included the code I used to create and fill the object without Automapper, just to show what's needed.
In my controller
var newInstructorProgram = new InstructorProgram() {
Name = newInstructorProgramDto.Name,
Description = newInstructorProgramDto.Description,
Length = (EventLength)newInstructorProgramDto.Length,
Experience = (Experience)newInstructorProgramDto.Experience,
Style = (YogaStyle)newInstructorProgramDto.Style,
Calories = newInstructorProgramDto.Calories,
InstructorId = userFromRepo.Id
};
foreach(var poseId in newInstructorProgramDto.Poses) {
var newPose = new InstructorProgramPose() { PoseId = poseId };
newInstructorProgram.Poses.Add(newPose);
}
_unitOfWork.Repository<InstructorProgram>().Add(newInstructorProgram);
I implemented a solution last night so I'm posting it here.
I added an extension method to the mapping shown below
CreateMap<InstructorProgramForUpdateDto, InstructorProgram>()
.ForMember(d => d.Poses, opt => opt.MapFrom(s => s.Poses.GetInstructorProgramPoses<InstructorProgramPose>()));
Here is the method in the extension class
public static ICollection<T> GetInstructorProgramPoses<T>(this ICollection<int> poses) where T : IInstructorProgramPose, new() {
var instructorProgramPoseColllection = new List<T>();
foreach(var poseId in poses) {
T pose = new T();
pose.PoseId = poseId;
instructorProgramPoseColllection.Add(pose);
}
return instructorProgramPoseColllection;
}

Complex Object to List Mapping in automapper - RESTAPI

Below is my Source object
public class Source
{
public string Name { get; set; }
public DateTime GeneratedFrom { get; set; }
public DateTime GeneratedTo { get; set; }
}
Then the Destination object
public class Destination
{
public DateTime Day{ get; set; }
public string Name { get; set; }
}
The Date in the destination gets its value from Source.GenerateFrom and Source.GenerateTo (iterating until Source.Generatefrom <= Source.GenerateTousing while loop). Finally, I need a list of objects as below.
{"Day":"2019-02-04","name":"abc"},
{"Day":"2019-02-05","name":"abc"},
{"Day":"2019-02-06","name":"abc"}
Can this be achieved using automapper?
Below is the mapper profile class with the whileloop. Not sure if this is the right approach
CreateMap<Source, Destination>()
.forMember(
dest => dest.Day,
opt => opt.MapFrom(src => src.GenerateFrom))
.AfterMap((src, dest) =>
{
var StartDate = src.GenerateFrom;
var EndDate = src.GenerateTo;
List<Destination> list = new List<Destination>();
while(Startdate <= EndDate)
{
var data = new Destination {Day = StartDate, name = src.Name};
list.Add(data);
StartDate.AddDays(1);
}
});
You can register this mapping for Your objects, if Day always get it's value from GeneratedFrom property. (Or modify it according to Your needs)
CreateMap<Source, Destination>()
.ForMember(d => d.GeneratedFrom, o => o.MapFrom(s => s.Day))
.ForMember(d => d.Name, o => o.MapFrom(s => s.Name));
Then If You want to get a list of objects where Source.Generatefrom <= Source.GenerateTo You need to use System.Linq expression:
var destList = sourceList.Where(x => x.GeneratedFrom <= x.GeneratedTo)
.Select(_mapper.Map<Destination>).ToList();
Then serialise it using Newtonsoft.Json library:
var json = JsonConvert.SerializeObject(destList);

How to ignore property of property in AutoMapper mapping?

Image a Person and a Group class with a many-to-many relationship. A person has a list of groups and a group has a list of people.
When mapping Person to PersonDTO I have a stack overflow exception because AutoMapper can't handle the Person>Groups>Members>Groups>Members>...
So here's the example code:
public class Person
{
public string Name { get; set; }
public List<Group> Groups { get; set; }
}
public class Group
{
public string Name { get; set; }
public List<Person> Members { get; set; }
}
public class PersonDTO
{
public string Name { get; set; }
public List<GroupDTO> Groups { get; set; }
}
public class GroupDTO
{
public string Name { get; set; }
public List<PersonDTO> Members { get; set; }
}
When I use .ForMember in creating a mapper, the first mapper that gets executed throws a null reference exception.
Here's the code for the mapper:
CreateMap<Person, PersonDTO>()
.ForMember(x => x.Groups.Select(y => y.Members), opt => opt.Ignore())
.ReverseMap();
CreateMap<Group, GroupDTO>()
.ForMember(x => x.Members.Select(y => y.Groups), opt => opt.Ignore())
.ReverseMap();
So what am I missing or doing wrong? When I remove the .ForMember methods, the null reference exception is not thrown anymore.
UPDATE: I really want to emphasize the main point of my question is how to ignore a property of a property. This code is just a rather simple example.
UPDATE 2: This is how I fixed it, big thanks to Lucian-Bargaoanu
CreateMap<Person, PersonDTO>()
.ForMember(x => x.Groups.Select(y => y.Members), opt => opt.Ignore())
.PreserveReferences() // This is the solution!
.ReverseMap();
CreateMap<Group, GroupDTO>()
.ForMember(x => x.Members.Select(y => y.Groups), opt => opt.Ignore())
.PreserveReferences() // This is the solution!
.ReverseMap();
Thanks to .PreserveReferences() the circular references get fixed!
This should just work. See https://github.com/AutoMapper/AutoMapper/wiki/5.0-Upgrade-Guide#circular-references. There is also a PR pending https://github.com/AutoMapper/AutoMapper/pull/2233.
I think the problem you are experiencing comes from wrong assumption that Groups in PersonDTO.Groups are the same as GroupDTO - it cannot be so without the infinite dependency loop. The following code should work for you:
CreateMap<Person, PersonDTO>()
.ForMember(x => x.Groups, opt => opt.Ignore())
.ReverseMap()
.AfterMap((src, dest) =>
{
dest.Groups = src.Groups.Select(g => new GroupDTO { Name = g.Name }).ToList()
});
CreateMap<Group, GroupDTO>()
.ForMember(x => x.Members, opt => opt.Ignore())
.ReverseMap()
.AfterMap((src, dest) =>
{
dest.Members = src.Members.Select(p => new PersonDTO { Name = p.Name }).ToList()
});
You basically need to teach AutoMapper that in case of PersonDTO.Groups property it should map GroupDTO objects differently.
But I think that your problem is more like architectural issue than code one. PersonDTO.Groups should not be of type GroupDTO - you are here only interested in groups particular user belongs to and not other members of his groups. You should have some simpler type like:
public class PersonGroupDTO
{
public string Name { get; set; }
}
(the name is up to you of course) to only identify the group without passing additionally members.

AutoMapper Conditional Entity Mapping

I have a db entity which stores the Order Addresses like this...
And I have the BLL classes like this...
public class DeliveryAddress
{
public string Id { get; set; }
public string PersonyName { get; set; }
public string CompanyName { get; set; }
public List<string> AddressLines { get; set; }
public string Zip { get; set; }
public string City { get; set; }
public string CountryCode { get; set; }
}
and another class like this...
public class InvoiceAddress
{
public string Id { get; set; }
public string PersonyName { get; set; }
public string CompanyName { get; set; }
public List<string> AddressLines { get; set; }
public string Zip { get; set; }
public string City { get; set; }
public string CountryCode { get; set; }
}
and I want to map the EF entity to the above classes on the basis of AddressType column. Can anybody explain me how to do that ?
UPDATE
I want to map to OR.DeliveryAddress if the addressType is "Delivery" and to OR.InvoiceAddress if the addressType is "Invoice"
So far, I have been able to do this, but I don't know how to apply condition on the entity mapping level...
Mapper.CreateMap<OrderAddress, OR.DeliveryAddress>()
.ForMember(d => d.City, o => o.MapFrom(s => s.city))
.ForMember(d => d.CompanyName, o => o.UseValue(string.Empty))
.ForMember(d => d.CountryCode, o => o.MapFrom(s => s.countryCode))
.ForMember(d => d.Id, o => o.MapFrom(s => s.id))
.ForMember(d => d.PersonyName, o => o.MapFrom(s => s.name))
.ForMember(d => d.Zip, o => o.MapFrom(s => s.zip));
UPDATE 2
After discussion with #Yuliam Here is the Fiddle that I could come up with for my problem...
You can create a customer mapper to object. And also you don't have to specify each property using ForMember because if the difference is only upper case / lower case (unless for PersonName), by default AutoMapper is case insensitive when mapping the property name.
Create a custom mapper to object.
public class AddressConverter : ITypeConverter<OrderAddress, object>
{
public object Convert(ResolutionContext context)
{
var o = context.SourceValue as OrderAddress;
if (o == null) return null;
if (o.addressType == "Delivery") return Mapper.Map<OR.DeliveryAddress>(o);
if (o.addressType == "Invoice") return Mapper.Map<OR.InvoiceAddress>(o);
return null;
}
}
Then define the mapper.
Mapper.CreateMap<OrderAddress, OR.DeliveryAddress>()
.ForMember(d => d.PersonyName, o => o.MapFrom(s => s.name));
Mapper.CreateMap<OrderAddress, OR.InvoiceAddress>()
.ForMember(d => d.PersonyName, o => o.MapFrom(s => s.name));
Mapper.CreateMap<OrderAddress, object>().ConvertUsing<AddressConverter>();
Usage.
var orderAddressDto = Mapper.Map<object>(orderAddress);
The actual orderAddressDto type will be based on the addressType. If you have an interface or base class for OR.DeliveryAddress and OR.InvoiceAddress that would be more strongly type. Then replace the object type with the interface / base class.
You may want to try taking a look at ResolveUsing
Semi-pseudo code, as I don't know what your entire Domain Model looks like:
Mapper.CreateMap<OrderObject, OrderDto>()
.ForMember(x => x.Address, opt => opt.ResolveUsing(oo => oo.Type == Invoice ? oo.InvoiceAddress : oo.DeliveryAddress));
I'm assuming here that you have an actual Order entity, which you're trying to make to an 'OrderDto' which only contains one address field.

Mapping one source class to multiple derived classes with automapper

Suppose i have a source class:
public class Source
{
//Several properties that can be mapped to DerivedBase and its subclasses
}
And some destination classes:
public class DestinationBase
{
//Several properties
}
public class DestinationDerived1 : DestinationBase
{
//Several properties
}
public class DestinationDerived2 : DestinationBase
{
//Several properties
}
Then I wish the derived destination classes to inherit the automapper configuration of the baseclass because I do not want to have to repeat it, is there any way to achieve this?
Mapper.CreateMap<Source, DestinationBase>()
.ForMember(...)
// Many more specific configurations that should not have to be repeated for the derived classes
.ForMember(...);
Mapper.CreateMap<Source, DestinationDerived1 >()
.ForMember(...);
Mapper.CreateMap<Source, DestinationDerived2 >()
.ForMember(...);
When I write it like this it does not use the base mappings at all, and include doesn't seem to help me.
Edit:
This is what I get:
public class Source
{
public string Test { get; set; }
public string Test2 { get; set; }
}
public class DestinationBase
{
public string Test3 { get; set; }
}
public class DestinationDerived1 : DestinationBase
{
public string Test4 { get; set; }
}
public class DestinationDerived2 : DestinationBase
{
public string Test5 { get; set; }
}
Mapper.CreateMap<Source, DestinationBase>()
.ForMember(d => d.Test3, e => e.MapFrom(s => s.Test))
.Include<Source, DestinationDerived1>()
.Include<Source, DestinationDerived2>();
Mapper.CreateMap<Source, DestinationDerived1>()
.ForMember(d => d.Test4, e => e.MapFrom(s => s.Test2));
Mapper.CreateMap<Source, DestinationDerived2>()
.ForMember(d => d.Test5, e => e.MapFrom(s => s.Test2));
AutoMapper.AutoMapperConfigurationException :
Unmapped members were found. Review the types and members below.
Add a custom mapping expression, ignore, add a custom resolver, or modify the source/destination type
Source -> DestinationDerived1 (Destination member list)
Test3
Include derived mappings into base mapping:
Mapper.CreateMap<Source, DestinationBase>()
.ForMember(d => d.Id, op => op.MapFrom(s => s.Id)) // you can remove this
.Include<Source, DestinationDerived1>()
.Include<Source, DestinationDerived2>();
Mapper.CreateMap<Source, DestinationDerived1>()
.ForMember(d => d.Name, op => op.MapFrom(s => s.Text))
.ForMember(d => d.Value2, op => op.MapFrom(s => s.Amount));
Mapper.CreateMap<Source, DestinationDerived2>()
.ForMember(d => d.Value, op => op.MapFrom(s => s.Amount));
Usage:
Mapper.AssertConfigurationIsValid();
var s = new Source() { Id = 2, Amount = 10M, Text = "foo" };
var d1 = Mapper.Map<DestinationDerived1>(s);
var d2 = Mapper.Map<DestinationDerived2>(s);
See Mapping inheritance on AutoMapper wiki.
UPDATE: Here is full code of classes which works as it should.
public class Source
{
public int Id { get; set; }
public string Text { get; set; }
public decimal Amount { get; set; }
}
public class DestinationBase
{
public int Id { get; set; }
}
public class DestinationDerived1 : DestinationBase
{
public string Name { get; set; }
public decimal Value2 { get; set; }
}
public class DestinationDerived2 : DestinationBase
{
public decimal Value { get; set; }
}
UPDATE (workaround of AutoMapper bug):
public static class Extensions
{
public static IMappingExpression<Source, TDestination> MapBase<TDestination>(
this IMappingExpression<Source, TDestination> mapping)
where TDestination: DestinationBase
{
// all base class mappings goes here
return mapping.ForMember(d => d.Test3, e => e.MapFrom(s => s.Test));
}
}
And all mappings:
Mapper.CreateMap<Source, DestinationBase>()
.Include<Source, DestinationDerived1>()
.Include<Source, DestinationDerived2>()
.MapBase();
Mapper.CreateMap<Source, DestinationDerived1>()
.MapBase()
.ForMember(d => d.Test4, e => e.MapFrom(s => s.Test2));
Mapper.CreateMap<Source, DestinationDerived2>()
.MapBase()
.ForMember(d => d.Test5, e => e.MapFrom(s => s.Test2));
For Automapper 8.0.
Current version has new method IncludeAllDerived
Here's working example:
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Source, DestinationBase>()
.ForMember(dest => dest.Test3, opt => opt.MapFrom(src => src.Test))
.IncludeAllDerived();
cfg.CreateMap<Source, DestinationDerived1>()
.ForMember(dest => dest.Test4, opt => opt.MapFrom(src => src.Test2));
cfg.CreateMap<Source, DestinationDerived2>()
.ForMember(dest => dest.Test5, opt => opt.MapFrom(src => src.Test2));
});
var mapper = config.CreateMapper();
var source = new Source { Test = "SourceTestProperty", Test2 = "SourceTest2Property" };
var d1 = mapper.Map<DestinationDerived1>(source);
var d2 = mapper.Map<DestinationDerived2>(source);
Assert.Equal("SourceTestProperty", d1.Test3);
Assert.Equal("SourceTest2Property", d1.Test4);
Assert.Equal("SourceTestProperty", d2.Test3);
Assert.Equal("SourceTest2Property", d2.Test5);
NB! For those who are having issues with derived interfaces. AutoMapper does not support registering against derived interfaces. Only classes are handled.
To make it work, you have to change your type reference for CreateMap to the class instead of interface.
Example:
interface Interface1 {}
class Class1: Interface1 {}
interface Interface2: Interface1 {}
class Class2: Class1, Interface2 {}
CreateMap<OtherClass, Interface1>().IncludeAllDerived();
CreateMap<OtherClass, Interface2>();
Any mapping against Interface2 will only use the first CreateMap. You will have to identify the second as
CreateMap<OtherClass, Class2>();

Categories

Resources