automapper mapping property stays null - c#

it seems like i cannot get nested mappings working properly
after reading: http://docs.automapper.org/en/stable/Nested-mappings.html it should be very easily.
i have the following classes:
public class CatalogVehicle : VehicleBase
{
public string Type { get; set; }
public int Year { get; set; }
public VehicleSpecification VehicleSpecification { get; set; } = new VehicleSpecification();
} //removed some properties for readability
public class VehicleSpecification
{
public Engine Engine { get; set; } = new Engine();
public Transmission Transmission { get; set; } = new Transmission();
public int Co2 { get; set; }
public int Weight { get; set; }
} //again removed some more properties (all classes)
for mapping:
CreateMap<VehicleAndQuote, CatalogVehicle>()
.ForMember(catalogVehicle => catalogVehicle.Id,
source => source.MapFrom(vehicleAndQuote => vehicleAndQuote.Quote.QuotationIdentifier.Trim()))
.ForMember(catalogVehicle => catalogVehicle.Make,
source => source.MapFrom(vehicleAndQuote => vehicleAndQuote.Vehicle.VehicleMakeName))
.ForMember(catalogVehicle => catalogVehicle.Model,
source => source.MapFrom(vehicleAndQuote => vehicleAndQuote.Vehicle.VehicleModelTypeName))
.ForMember(catalogVehicle => catalogVehicle.VehicleSpecification, opt => opt.Ignore()); //removed some lines
CreateMap<VehicleAndQuote, VehicleSpecification>()
.ForMember(vehicleSpecification => vehicleSpecification.Co2, src => src.MapFrom(vehicleAndQuote => vehicleAndQuote.Vehicle.Co2.ToSafeInt()))
.ForMember(vehicleSpecification => vehicleSpecification.Weight, src => src.MapFrom(vehicleAndQuote => vehicleAndQuote.Vehicle.Weight.ToSafeInt()))
.ForMember(vehicleSpecification => vehicleSpecification.Rating, opt => opt.Ignore())
.ForMember(vehicleSpecification => vehicleSpecification.Tyres, opt => opt.Ignore()) //removed some lines as well
CreateMap<VehicleAndQuote, Engine>()
.ForMember(engine => engine.Displacement, opt => opt.Ignore())
.ForMember(engine => engine.Fuel, src => src.MapFrom(vehicleAndQuote => vehicleAndQuote.Vehicle.FuelType))
.ForMember(engine => engine.Power, src => src.ResolveUsing(GetEnginePower))
.ForMember(engine => engine.Cylinders, src => src.MapFrom(vehicleAndQuote => vehicleAndQuote.Vehicle.Cylinders));
//etc
as you can see i am ignoring the some properties because otherwise i get the unmapped properties error. After reading the article it should work, as long as you have all classes mapped.
i am calling the method like: var vehicle = _mapper.Map<VehicleAndQuote, CatalogVehicle>(vehicleAndQuote); <= this is the big class that contains all the information
so from mapping from VehicleAndQuote to CatalogVehicle -first few properties work properly, but then the mapping to the VehicleSpecification lies my problem. that one will not be populated properly...
does anyone see the problem?

You need to configure mapping for nested classes instead of ignoring them:
.ForMember(catalogVehicle => catalogVehicle.VehicleSpecification, src => src.MapFrom(vehicleAndQuote => vehicleAndQuote);
instead of
.ForMember(catalogVehicle => catalogVehicle.VehicleSpecification, opt => opt.Ignore());
And the same for other mappings

Related

Automapper - Projecting multiple properties from conditionally selected list

I want to avoid duplicating the src.Statuses.Where(s => s.StatusType!.StatusGroupId == 1).OrderByDescending(s => s.CreatedUTC).First() from the below mapping. Without changing the shape of my destinationDto.
I'm aware that I could change the DestinationDto to hold a "StatusDto" object which could then have it's own projection defined, and achieve it that way.
Does automapper have some syntax to do this without having to create extra dto's that reflect the source structure?
Basically a way to say
var status = src.Statuses.Where(s => s.StatusType!.StatusGroupId == 1).OrderByDescending(s => s.CreatedUTC).First()
and then use that status across multiple .ForCtorParam
The source looks like this:
Source 1 -- 0..N Status
Status 0..N -- 1 StatusType
StatusType 0..N -- 1 StatusGroup
public class DestinationDto
{
public DestinationDto(...)
public int Id { get; set; }
public DateTime StatusDate { get; set; }
public string Open { get; set; }
public string Status { get; set; }
public DateTime Created { get; set; }
}
public class DestinationProfile : Profile
{
public DestinationProfile()
{
CreateProjection<SourceType, DestinationDto>(MemberList.Destination)
.ForCtorParam(nameof(DestinationDto.Id), opt => opt.MapFrom(src => src.Id))
.ForCtorParam(nameof(DestinationDto.StatusDate),
opt => opt.MapFrom(src => src.Statuses.Where(s => s.StatusType!.StatusGroupId == 1).OrderByDescending(s => s.CreatedUTC).First().CreatedUTC))
.ForCtorParam(nameof(DestinationDto.Open),
opt => opt.MapFrom(src => src.Statuses.Where(s => s.StatusType!.StatusGroupId == 1).OrderByDescending(s => s.CreatedUTC).First().StatusType!.IsOpen))
.ForCtorParam(nameof(DestinationDto.Status),
opt => opt.MapFrom(src => src.Statuses.Where(s => s.StatusType!.StatusGroupId == 1).OrderByDescending(s => s.CreatedUTC).First().StatusType!.Label))
.ForCtorParam(nameof(DestinationDto.Created), opt => opt.MapFrom(src => src.CreatedUTC));
}
}

AutoMapper condition Mapping Issue

I have got a problem using Automapper when conditionally Mapping a table.
Here is an example:
public class DepositsVm : IMapFrom<Deposits>
{
public long DepId { get; set; }
public AddressDto Address { get; set; }
public void Mapping(Profile profile)
{
profile.CreateMap<Deposits, DepositsVm>()
.ForMember(d => d.DepId, s => s.MapFrom(s => s.DepId))
.ForMember(d => d.Address, opt => opt.MapFrom(s => s.ProcessingId != null ? s.DataProcessing.GridCollect.Grid.Address : s.Reduction.DataCollect.Tower.Address));
}
}
This results in an Object reference error.
I can see we can use https://docs.automapper.org/en/stable/Conditional-mapping.html#preconditions but this allow to check for only one condition. I expect to map a table using different join condition in failure and success scenarios.
But this works, because I'm projecting each Address separately. But, this is not desired. Because both are from Same Address Table
public class DepositsVm : IMapFrom<Deposits>
{
public long DepId { get; set; }
public AddressDto GridAddress { get; set; }
public AddressDto TowerAddress { get; set; }
public void Mapping(Profile profile)
{
profile.CreateMap<Deposits, DepositsVm>()
.ForMember(d => d.DepId, s => s.MapFrom(s => s.DepId))
.ForMember(d => d.GridAddress, opt => opt.MapFrom(s => s.DataProcessing.GridCollect.Grid.Address));
.ForMember(d => d.TowerAddress, opt => opt.MapFrom(s => s.Reduction.DataCollect.Tower.Address));
}
}
Either some part of s.DataProcessing.GridCollect.Grid.Address is null, or some part of s.Reduction.DataCollect.Tower.Address is null

AutoMapper, mapping one type in two different types

I have the following models:
public class Stuff
{
...
public IList<Place> Places { get; set; } = null!;
...
}
public class Place
{
...
public IList<Stuff> Stuffs { get; set; } = null!;
...
}
public class StuffEntity
{
...
public IList<PlaceStuffEntity> Places { get; set; } = null!;
...
}
public class PlaceEntity
{
...
public IList<PlaceStuffEntity> Stuffs { get; set; } = null!;
...
}
public class PlaceStuffEntity
{
public int StuffId { get; private set; }
public StuffEntity Stuff { get; set; } = null!;
public int PlaceId { get; private set; }
public PlaceEntity Place { get; set; } = null!;
}
cfg.CreateMap<StuffEntity, Stuff>()
.ForMember(d => d.Places,
opt => opt.MapFrom(s => s.Places.Select(y => y.Place).ToList()));
cfg.CreateMap<PlaceEntity, Place>()
.ForMember(d => d.Stuffs,
opt => opt.MapFrom(s => s.Places.Select(y => y.Stuff).ToList()));
cfg.CreateMap<PlaceAndStuffEntity, Stuff>() // < -- Issue
.IncludeMembers(entity=> entity.Stuff);
cfg.CreateMap<PlaceAndStuffEntity, Place>() // < -- Issue
.IncludeMembers(entity=> entity.Place);
by some reason when I add both last lines, conversion does not work ...
But if I add only one line for example for converting PlaceAndStuffEntity -> Stuff works only one conversion from PlaceEntity -> Place
var place = mapper.Map<Place>(placeEntity); // <- This works
var stuff = mapper.Map<Stuff>(stuffEntity); // <- Does not work !!
Is there a way properly handle the following conversions ?
It sounds like you want to map through the joining table (PlaceAndStuff) to get to the other entity type. For instance in your Place to get a list of Stuff, and Stuff to get a list of Place, you want to direct Automapper how to navigate through the joining table.
For instance:
cfg.CreateMap<StuffEntity, Stuff>()
.ForMember(x => x.Places, opt => opt.MapFrom(src => src.PlaceEntity));
// Where StuffEntity.Places = PlaceAndStuffEntities, to map Stuff.Places use PlaceAndStuffs.PlaceEntity
cfg.CreateMap<PlaceEntity, Place>()
.ForMember(x => x.Stuffs, opt => opt.MapFrom(src => src.StuffEntity));
So rather than trying to tell EF how to map the joining entity PlaceStuffEntity, we focus on the PlaceEntity and StuffEntity, and tell Automapper to navigate through the joining entity to get at the actual Stuff and Place relatives via the joining entity.
Change
cfg.CreateMap<PlaceEntity, Place>()
.ForMember(d => d.Stuffs,
opt => opt.MapFrom(s => s.Places.Select(y => y.Stuff).ToList()));
to
cfg.CreateMap<PlaceEntity, Place>()
.ForMember(d => d.Stuffs,
opt => opt.MapFrom(s => s.Stuffs.Select(y => y.Stuff).ToList()));
Source type PlaceEntity does not have a property named Places, only Stuffs.

Automapper - How to map to IEnumerable

I'm making Forum system, I have SubCategoryThreadsViewModel in which I'm trying to map LastComment and Date of last post for every thread. This is my code:
public class SubCategoryThreadsViewModel : IHaveCustomMappings
{
public string Title { get; set; }
public string Description { get; set; }
public IEnumerable<Thread> Threads { get; set; }
public ThreadInfoSubCategoryViewModel ThreadInfoSubCategoryViewModel { get; set; }
public void CreateMappings(IConfiguration configuration)
{
configuration.CreateMap<Thread, SubCategoryThreadsViewModel>()
.ForMember(m => m.Title, opt => opt.MapFrom(t => t.SubCategory.Title))
.ForMember(m => m.Description, opt => opt.MapFrom(t => t.SubCategory.Description))
.ForMember(m => m.Threads, opt => opt.MapFrom(t => t.SubCategory.Threads))
.ForMember(m => m.ThreadInfoSubCategoryViewModel, opt => opt.MapFrom(t => new ThreadInfoSubCategoryViewModel()
{
LastCommentBy = t.Posts.Select(a => a.Author.UserName),
DateOfLastPost = t.Posts.Select(a => a.CreatedOn.ToString()),
}))
.ReverseMap();
}
The code
.ForMember(m => m.ThreadInfoSubCategoryViewModel, opt => opt.MapFrom(t => new ThreadInfoSubCategoryViewModel()
{
LastCommentBy = t.Posts.Select(a => a.Author.UserName),
DateOfLastPost = t.Posts.Select(a => a.CreatedOn.ToString()),
}))
is working but only when property ThreadInfoSubCategoryViewModel is not Ienumerable as in the code above, and inside are two IEnumerable strings.
public class ThreadInfoSubCategoryViewModel
{
public IEnumerable<string> LastCommentBy { get; set; }
public IEnumerable<string> DateOfLastPost { get; set; }
}
This works, but I want ThreadInfoSubCategoryViewModel to be Ienumerable, and in the class properties to be string for easy foreach.
I have tried to make it IEnumerable, but with current automapper code it doesn't work.
You need to manually map the member to an IEnumerable<ThreadInfoSubCategoryViewModel> rather than a single object.
I assume each Post in t.Posts represents one ThreadInfoSubCategoryViewModel, so a simple Select() should do it:
public IEnumerable<ThreadInfoSubCategoryViewModel> ThreadInfoSubCategoryViewModel { get; set; }
...
.ForMember(m => m.ThreadInfoSubCategoryViewModel, opt => opt.MapFrom(t =>
t.Posts.Select(p => new ThreadInfoSubCategoryViewModel()
{
LastCommentBy = p.Author.UserName,
DateOfLastPost = p.CreatedOn.ToString()
})
))

AutoMapper and flattening nested arrays

I'm trying to use AutoMapper to flatten multiple levels of arrays.
Consider the following source classes:
class X {
public string A { get; set; }
public Y[] B { get; set; }
}
class Y {
public string C { get; set; }
public Z[] D { get; set; }
}
class Z {
public string E { get; set; }
public string F { get; set; }
}
And the following destination:
class Destination {
public string A { get; set; }
public string C { get; set; }
public string E { get; set; }
public string F { get; set; }
}
What I'd like to be able to do is get a List from one or more X, e.g.:
Mapper.Map<IEnumerable<X>, IEnumerable<Destination>>(arrayOfX);
I'm unable to figure out what sort of mapping configuration to use to achieve this. MapFrom seems like the way to go for 1:1 compositions, but doesn't seem to be able to handle the array (or other enumerable) unless I use AutoMapper's destination naming convention.
Any insights on how to achieve this?
Try this mapper,
Mapper.CreateMap<Z, Destination>();
Mapper.CreateMap<Y, Destination>();
Mapper.CreateMap<X, Destination>()
.ForMember(destination => destination.A, options => options.MapFrom(source => source.A)).IgnoreAllNonExisting()
.ForMember(destination => destination.C, options => options.MapFrom(source => Mapper.Map<IEnumerable<Y>, IEnumerable<Destination>>(source.B).FirstOrDefault().C))
.ForMember(destination => destination.E, options => options.MapFrom(source => Mapper.Map<IEnumerable<Z>, IEnumerable<Destination>>(source.B.SelectMany(d => d.D)).FirstOrDefault().E))
.ForMember(destination => destination.F, options => options.MapFrom(source => Mapper.Map<IEnumerable<Z>, IEnumerable<Destination>>(source.B.SelectMany(d => d.D)).FirstOrDefault().F));
var result = Mapper.Map<IEnumerable<X>, IEnumerable<Destination>>(arrayOfX);
I had a very similar problem a while ago. I had a collection of locations, and each location had a collection of streets. I wanted to map them to a collection of view models where each view model represented a street (including the location details).
This was my solution: https://groups.google.com/forum/#!topic/automapper-users/b66c1M8eS8E
For this particular problem, this could be your mapping configuration:
public static class AutoMapperConfig
{
public static void Configure()
{
Mapper.CreateMap<Z, Destination>()
.ForMember(dest => dest.A, opt => opt.Ignore())
.ForMember(dest => dest.C, opt => opt.Ignore());
Mapper.CreateMap<Y, Destination>()
.ForMember(dest => dest.A, opt => opt.Ignore())
.ForMember(dest => dest.E, opt => opt.Ignore())
.ForMember(dest => dest.F, opt => opt.Ignore());
Mapper.CreateMap<X, Destination>()
.ForMember(dest => dest.C, opt => opt.Ignore())
.ForMember(dest => dest.E, opt => opt.Ignore())
.ForMember(dest => dest.F, opt => opt.Ignore());
}
}
Because AutoMapper is primarily a 1:1 mapping, you need to implement a wee bit of magic to map to multiple objects. This is an example of how you could call that mapping to populate your object:
var rc = data.SelectMany(
x => x.B.SelectMany(
y => y.D
.Select(Mapper.Map<Z, Destination>)
.Select(z => Mapper.Map(y, z))
)
.Select(y => Mapper.Map(x, y))
);
Here are a couple of unit tests to validate the mapping and show it in action:
[TestFixture]
public class MapperTests
{
[Test]
public void Mapping_Configuration_IsValid()
{
AutoMapperConfig.Configure();
Mapper.AssertConfigurationIsValid();
}
[Test]
public void Mapping_TestItems_MappedOK()
{
AutoMapperConfig.Configure();
Mapper.AssertConfigurationIsValid();
var data = new[]
{
new X
{
A = "A1",
B = new[]
{
new Y
{
C = "A1C1",
D = new[]
{
new Z
{
E = "A1C1E1",
F = "A1C1F1"
},
new Z
{
E = "A1C1E2",
F = "A1C1F2"
},
}
},
new Y
{
C = "A1C2",
D = new[]
{
new Z
{
E = "A1C2E1",
F = "A1C2F1"
},
new Z
{
E = "A1C2E2",
F = "A1C2F2"
},
}
}
}
}
};
var rc = data.SelectMany(
x => x.B.SelectMany(
y => y.D
.Select(Mapper.Map<Z, Destination>)
.Select(z => Mapper.Map(y, z))
)
.Select(y => Mapper.Map(x, y))
);
Assert.That(rc, Is.Not.Null);
Assert.That(rc.Count(), Is.EqualTo(4));
var item = rc.FirstOrDefault(x => x.F == "A1C2F2");
Assert.That(item, Is.Not.Null);
Assert.That(item.A, Is.EqualTo("A1"));
Assert.That(item.C, Is.EqualTo("A1C2"));
Assert.That(item.E, Is.EqualTo("A1C2E2"));
Assert.That(item.F, Is.EqualTo("A1C2F2"));
}
}
For anyone else coming across this post by searching how to Flatten object structure with AutoMapper - the new AutoMapper supports flattening with IncludeMembers() syntax.
Source: http://docs.automapper.org/en/stable/Flattening.html
So the original issue could be solved like this:
Mapper.CreateMap<Z, Destination>();
Mapper.CreateMap<Y, Destination>().IncludeMembers(src => src.D);
Mapper.CreateMap<X, Destination>().IncludeMembers(src => src.B);

Categories