AutoMapper creates 'ghost' objects when unflattening 3 level hierarchy - c#

We are trying to flatten a nested hierarchy like Order -may have a-> Customer -has an-> Address into a model that contains all these entities' models as a root property like this:
public class FlatModel
{
public OrderModel Order { get; set; }
public CustomerModel Customer { get; set; }
public AddressModel CustomerAddress { get; set; }
}
The flattening works as expected with this mapping profile:
public class FlatModelProfile : Profile
{
public FlatModelProfile()
{
CreateMap<Order, OrderModel>().ReverseMap();
CreateMap<Customer, CustomerModel>().ReverseMap();
CreateMap<Address, AddressModel>().ReverseMap();
CreateMap<Order, FlatModel>()
.ForMember(m => m.Order, opt => opt.MapFrom(p => p))
.ForMember(m => m.Customer, opt => opt.MapFrom(p => p.Customer))
.ForMember(m => m.CustomerAddress, opt => opt.MapFrom(p => p.Customer.Address))
.ReverseMap();
}
}
However, when we unflatten the FlatModel back into an Order when the users submits their changes, the Customer property will not be null, even though the Customer property on the FlatModel is.
The following gist shows how to reproduce the issue: https://gist.github.com/luetm/6c4caf62f76e430ad74dd13adf82c451
How to we have to setup the mapping so the Customer property on Order stays null?

Related

How can I include a child object mapping using AutoMapper when the source objects have no inheritance relationship?

I have an object model something like this:
public class Concert {
public Band Band { get; set; }
public ConcertVenue Venue { get; set; }
}
public class TicketOrder {
public Concert Concert { get; set; }
public string CustomerName { get; set; }
}
// DTOs for email and web views:
public class ConcertDto {
public string Artist { get; set; }
public string Venue { get; set; }
}
public class TicketOrderDto : ConcertDto {
public string CustomerName { get; set; }
}
I'm using AutoMapper to map domain objects to DTOs. The DTOs here have an inheritance relationship that doesn't exist in the domain model (because when I send an email about a ticket order, I want to include all the information about the concert)
I have a mapping defined like this:
config.CreateMap<Concert, ConcertDto>()
.ForMember(dto => dto.Artist, opt => opt.MapFrom(concert => concert.Band.Name))
.ForMember(dto => dto.Venue, opt => opt.MapFrom(concert => concert.GetVenueSummary());
config.CreateMap<TicketOrder, ConcertDto>()
.ForMember(dto => dto.Artist, opt => opt.MapFrom(concert => concert.Band.Name))
.ForMember(dto => dto.Venue, opt => opt.MapFrom(concert => concert.GetVenueSummary())
.ForMember(dto => dto.CustomerName, optn.MapFrom(order => order.Customer.FullName))
;
There's some duplication in those maps, and what I want to do is to reuse the Concert > ConcertViewData mapping when I map the TicketOrderDto:
cfg.CreateMap<TicketOrder, TicketOrderDto>()
// This is what I *want* but isn't valid AutoMapper syntax:
.IncludeMembers(dto => dto, order => order.Concert)
.ForMember(dto => dto.CustomerName, optn.MapFrom(order => order.Customer.FullName));
but this fails with:
System.ArgumentOutOfRangeException: Only member accesses are allowed.
dto => dto (Parameter 'memberExpressions')
at AutoMapper.ExpressionExtensions.EnsureMemberPath(LambdaExpression exp, String name)
Calling .IncludeBase<> doesn't work, because ConcertOrder doesn't derive from Concert.
Is there an easy way to import one map into another but specify that it should map from a child object of the source type? i.e. "hey, please map source.Child onto this, and then run the regular source > this mapping?"
I am going to make an assumption here, but I believe the mapping should be from TicketOrder to TicketOrderDto, and not ConcertDto (which contains no CustomerName property) as the given models don't match the given mapping configuration.
In that case, you should be able to use .AfterMap() on ticket mapper configuration to map from Concert to ConcertDto.
cfg.CreateMap<TicketOrder, TicketOrderDto>()
.ForMember(d => d.CustomerName, o => o.MapFrom(s => s.Order.Customer.Name))
.AfterMap((s, d, context) => context.Mapper.Map(s.Concert, d));

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

How to map a DTO with a many-to-many relationship to a EF core entity with a relationship table using automapper?

I have two entities in my DB. A and B. I also have a relationship table representing a many to many relation between these entities AB:
public class A
{
public ICollection<AB> ABs { get; set; }
}
public class B
{
public ICollection<AB> ABs { get; set; }
}
public class AB
{
public A A { get; set; }
public B B { get; set; }
}
Now I want to hide this relation table in my dto like so:
public class ADTO
{
public ICollection<B> Bs { get; set; }
}
I want a single collection of Bs with my instances represented directly, not as a relation table. I want to have an automapper profile that maps from a list of entities B to a list of previously not existing entities AB with B as an attribute, as well as the instance A itself.
I already have implemented the mapper from A to ADTO like so:
public class AProfile : Profile
{
public AProfile()
{
CreateMap<A, ADTO>()
.ForMember(dest => dest.B, opt => opt.MapFrom(src => src.AB.Select(y => y.B).ToList()));
CreateMap<ADTO, A>();
}
}
What I am missing is the reverse direction: From ADTO with its list of entities to A with its reference to the relationship table entity.
Make your profile like below:
CreateMap<ADTO, A>()
.ForMember(dest => dest.AName, opt => opt.MapFrom(src => src.AName))
.ForMember(dest => dest.ABs, opt => opt.MapFrom(src => src.Bs))
.AfterMap((src, dest) =>{
foreach(var b in dest.ABs)
{
b.AId = src.Id;
}
});
CreateMap<B, AB>()
.ForMember(dest=>dest.BId,opt=>opt.MapFrom(src=>src.Id))
.ForMember(dest=>dest.B,opt=>opt.MapFrom(src=>src));

To map related entity property to a viewmodel property using Automapper

I have User Table, UserParents table, UserMarks table and UserGrades Table. I am trying to use automapper to map a few of the properties to my view model.
The User table Model :
public partial class User
{
public string UserId { get; set; }
public string UserName { get; set; }
public virtual ICollection<UserParents> UserParents { get; set; }
public virtual ICollection<UserMarks> UserMarks { get; set; }
public virtual ICollection<UserGrades> UserGrades { get; set; }
}
My ViewModel: This contains a portion of the fields from each of the four table.
public class UserViewModel
{
public string UserId{get;set;}
//From UserParents table
public string UserParentName{get;set;}
}
My query :
var user = context.User
.Include(i => i.UserParents)
.Include(i => i.UserMarks)
.Include(i => i.UserGrades)
.Where(i =>i.userId == userId).FirstOrDefault();
And automapper:
config = new MapperConfiguration(cfg => {
cfg.CreateMap<User,UserViewModel>()
//This works
.ForMember(m => m.UserId,opt =>opt.MapFrom(entity => entity.UserId))
//Can't map vm.UserParentName directly to entity.UserParents.UserParentName and so have to do all of this
.ForMember(vm => vm.UserParentName, opt => opt.MapFrom(entity => entity.UserParents.Select(c =>c.UserParentName).FirstOrDefault()))
.ReverseMap();});
IMapper mapper = config.CreateMapper();
So as in the commented portion of the code, why can't I directly map vm.UserParentName directly to entity.UserParents.UserParentName ?
Is there any other way of doing it?
Change your configuration like so:
config = new MapperConfiguration(cfg => {
cfg.CreateMap<User,UserViewModel>()
//This is actually unnecesseray
//.ForMember(m => m.UserId,opt =>opt.MapFrom(entity => entity.UserId))
// If you only want the first parent name - Not sure on structure of UserParent class so just assuming you have a field "Name"
.ForMember(vm => vm.UserParentName,
opt => opt.MapFrom(entity => entity.UserParents.FirstOrDefault().Name))
.ReverseMap();
});
IMapper mapper = config.CreateMapper();
The map betwen m.UserId and entity.User Id is not needed, Automapper will do this automatically.
The map for UserParentName, I'm not exactly sure why you would want to get the first in the list of them, but if that is definitely the case then just use the code above to fetch it.

resolving complex entity model to flat model view using AutoMapper

I want to create a map for a somewhat complex entity model to a flattened view Model
My entity model is like so
cbItems
has many cbItemsContent
has many cbRegulators
so my viewmodels are like so
for cbItems:
public class ItemViewModel
{
public ItemViewModel()
{
this.CbItemsContents = new HashSet<ItemContentViewModel>();
}
public int ItemID { get; set; }
......
public virtual ICollection<ItemContentViewModel> CbItemsContents { get; set; }
}
}
for cbItemsContent:
public class ItemContentViewModel
{
public int ItemContentID { get; set; }
public int ItemID { get; set; }
....
public ItemContentRegulatorsViewModel RegulatedBy { get; set; }
}
}
for cbRegulators:
public class ItemContentRegulatorsViewModel
{
public int ItemContentId { get; set; }
public IEnumerable<int> RegulatorIds { get; set; }
}
}
I had hoped it would be as easy as this:
config.CreateMap<CbItem, ItemViewModel>();
config.CreateMap<CbItemsContent, ItemContentViewModel>()
.ForMember(dest => dest.RegulatedBy.ItemContentId,
m => m.MapFrom(src => src.GenericID))
.ForMember(dest => dest.RegulatedBy.RegulatorIds,
n => n.MapFrom(src => src.cbItemsContentRegulators.Select(q => q.cbRegulator.RegulatorId)));
from teh following query:
ItemViewModel item =
_context.cbItems.Where(u => u.ItemId = id)
.ProjectTo<ItemViewModel>()
.first();
But this results in an error:
Expression 'dest => dest.RegulatedBy.ItemContentId' must resolve to
top-level member and not any child object's properties. Use a custom
resolver on the child type or the AfterMap option instead. Parameter
name: lambdaExpression
HOw can I achieve my desired model layout?
You have to map ItemContentRegulatorsViewModel, then you don't need to set it from the ViewModel above.
#Rabban probably means something like this:
config.CreateMap<CbItemsContent, ItemContentViewModel>()
.ForMember(dest => dest.RegulatedBy, o => o.MapFrom(src => src));
config.CreateMap<CbItemsContent, ItemContentRegulatorsViewModel>()
.ForMember(dest => dest.ItemContentId, o => o.MapFrom(src => src.GenericID))
.ForMember(dest => dest.RegulatorIds, o => o.MapFrom(src => src.cbItemsContentRegulators.Select(q => q.cbRegulator.RegulatorId)));

Categories