To map related entity property to a viewmodel property using Automapper - c#

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.

Related

AutoMapper creates 'ghost' objects when unflattening 3 level hierarchy

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?

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

Convert Dictionary to ICollection in Entity Framework Core

Here I am stucked with the conversion of dictionary to Icollection in EF Core. I have Dictionary in FlatEmployee class in which I am storing list of key, value pair in database. I have declared like this:
public class FlatEmployee
{
public int EmployeeId { get; set; }
public Dictionary<string, long> PayAndAllowances { get; set; }
}
//====================Configuration
public void Configure(EntityTypeBuilder<FlatEmployee> builder)
{
builder.HasKey(f => new { f.EmployeeId });
builder.Property(sb => sb.PayAndAllowances)
.HasConversion(
pa => JsonConvert.SerializeObject(pa),
pa => JsonConvert.DeserializeObject<Dictionary<string, long>>(pa));
}
This works absolutely fine when I am seeding or inserting. But the problem I am facing when I am trying to get the FlatEmployee class. This is because I want to get the dictionary in Collection. For that purpose I have declared another class like this:
public class LastPayCertificateViewModel: IHaveCustomMapping
{
public int EmployeeCode { get; set; }
public EmployeeEarningDTO PayAndAllowances { get; set; }
public void CreateMappings(Profile configuration)
{
configuration.CreateMap<FlatEmployee, LastPayCertificateViewModel>()
.ForMember(dto => dto.EmployeeCode , opt => opt.MapFrom(entity => entity.EmployeeId ));
}
}
public class EmployeeEarningDTO : IHaveCustomMapping
{
public ICollection<BaseEmployeeDictionary> PayAndAllowances { get; set; }
public void CreateMappings(Profile configuration)
{
configuration.CreateMap<FlatEmployee, EmployeeEarningDTO>()
.ForMember(dto => dto.PayAndAllowances, opt => opt.MapFrom(entity => entity.PayAndAllowances));
}
}
public class BaseEmployeeDictionary
{
public string Name { get; set; }
public long Amount { get; set; }
}
When I am trying to use above classes for getting the data like this:
public class LastPayCertificateQuery : IRequest<LastPayCertificateViewModel>
{
public string EmployeeCode { get; set; }
}
public async Task<LastPayCertificateViewModel> Handle(LastPayCertificateQuery request, CancellationToken cancellationToken)
{
var predicate = PredicateBuilder.False<FlatEmployee>();
predicate = predicate.Or(emp => emp.EmployeeId == request.EmployeeCode);
var employee = await _context.FlatEmployee
.Where(predicate)
.FirstOrDefaultAsync(cancellationToken);
if (employee == null)
return null;
var empVM = _mapper.Map<LastPayCertificateViewModel>(employee);
}
Then I am getting null in PayAndAllowances in empVM. This is what my problem is. Where is my problem? I thought it was because that Dictionary has key value pair and which is not been able to convert to BaseEmployeeDictionary. I have tried this way as well to add List Item to PayAndAllowances in empVM
foreach (KeyValuePair<string, long> data in employee.DeductionsByAdjustment)
{
BaseEmployeeDictionary listItem = new BaseEmployeeDictionary
{
Name = data.Key,
Amount = data.Value
};
empVM.EarningDetails.PayAndAllowances.Add(listItem);
return empVM;
}
Which of course wont work because the empVM.EarningDetails.PayAndAllowances is null and throws NullReferenceException. My queries is how to map between the Dictionary to ICollection while Creating Map in EmployeeEarningDTO. OR It would be really appretiated for your valuable suggestion and solution please.
It turns out to be AutoMapper mapping issue.
First, EmployeeEarningDTO inside LastPayCertificateViewModel creates additional level compared to FlatEmployee:
LastPayCertificateViewModel.PayPayAndAllowances.PayAndAllowances
vs
FlatEmployee.PayAndAllowances
AutoMapper maps by default properties with the same name. So inside FlatEmployee to LastPayCertificateViewModel map it would try to map Dictionary<string, long> PayAndAllowances to EmployeeEarningDTO PayAndAllowances. But there is no mapping from Dictionary<string, long> to EmployeeEarningDTO. Instead, there is a mapping from FlatEmployee to EmployeeEarningDTO, so you have to tell AM to use it:
configuration.CreateMap<FlatEmployee, LastPayCertificateViewModel>()
.ForMember(dto => dto.EmployeeCode, opt => opt.MapFrom(entity => entity.EmployeeId))
.ForMember(dto => dto.PayAndAllowances, opt => opt.MapFrom(entity => entity)); // <--
Second, mapping from FlatEmployee to EmployeeEarningDTO - AM will automatically try to map PayAndAllowances properties, but there is no mapping from KeyValuePair<string, long> to BaseEmployeeDictionary. You could define such mapping
configuration.CreateMap<KeyValuePair<string, long>, BaseEmployeeDictionary>()
.ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Key))
.ForMember(dst => dst.Amount, opt => opt.MapFrom(src => src.Value));
which will allow you to use simply
configuration.CreateMap<FlatEmployee, EmployeeEarningDTO>();
however you probably won't do that because you don't want every KeyValuePair<string, long> to be mapped to BaseEmployeeDictionary, so you could do that mapping inplace:
configuration.CreateMap<FlatEmployee, EmployeeEarningDTO>()
.ForMember(dto => dto.PayAndAllowances, opt => opt.MapFrom(entity => entity.PayAndAllowances
.Select(src => new BaseEmployeeDictionary { Name = src.Key, Amount = src.Value })));

Automapper - how to apply ForMember configuration at a deep level

I am trying to learn asp.NetCore 2.2. I am trying to setup a simple one page site. I have run into a problem with Automapper where manual Mappinng using forMember() is working at a top level for CreateMap<Listing, ListingSearchResultsDto>().ForMember(ListingPhotosUrl) but not at a lower level. I have another mapping CreateMap<User, UserDetailsDto>() where user contains an object Mylistings of type Listing. Mylistings is correctly auto mapped to ListingSearchResultsDto but manual configuration CreateMap<Listing, ListingSearchResultsDto>().ForMember(ListingPhotosUrl) is not applied.
I Have tried CreateMap<User, UserDetailsDto>().Formember(dest.Mylistings.ListingPhotosUrl,src.Mylistings.Photos.Url) but it seems that is not possible.
I Also tried this-> But no luck
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<User, UserDetailsDto>();
cfg.CreateMap<Listing, ListingSearchResultsDto>()
.ForMember(dest => dest.ListingPhotosUrl, opt =>
{
opt.MapFrom(src => src.Photos.FirstOrDefault(p => p.IsMain).Url);
});
});
var mapper = config.CreateMapper();
The Code:
AutoMappperProfiles
public AutoMapperProfiles()
{
CreateMap<Listing, ListingSearchResultsDto>()
.ForMember(dest => dest.ListingPhotosUrl, opt =>
{
opt.MapFrom(src => src.Photos.FirstOrDefault(p => p.IsMain).Url);
});
CreateMap<User, UserDetailsDto>();
CreateMap<ListingPhoto, ListingPhotosDetailedDto>();
}
User
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public ICollection<Listing> MyListings { get; set; }
}
UserDetailsDto
public class UserDetailsDto
{
public int Id { get; set; }
public string Username { get; set; }
public ICollection<ListingSearchResultsDto> MyListings { get; set;}
}
Listing
public int id { get; set; }
public string Category { get; set; }
public string Title { get; set; }
public ICollection<ListingPhoto> Photos { get; set; }
ListingSearchResultsDto
public class ListingSearchResultsDto
{
public int id { get; set; }
public string Title { get; set; }
public string ListingPhotosUrl { get; set; }
}
I am using CreateMap<Listing, ListingSearchResultsDto>().Formember(des,src) to manually map a destination property ListingPhotosUrl. I have another mapping CreateMap<User, UserDetailsDto>(). Inside User & UsedetailsDto classes i have a objects called MyListings of types ICollection<Listing> and ICollection<ListingSearchResultsDto> respectively. MyListings object is auto mapped correctly but ListingPhotosUrl manual mapping is not being applied. CreateMap<Listing,ListingSearchResultsDto>.Formember(des,src)) manual mapping is working at top level, but not at deeper level inside CreateMap<User, UserDetailsDto>(), is there anyway to fix this? thanks
FIXED - Automapper was working fine. Issue in Entity Framework DbContext. I did not include the photos as related data in the EF Core method for loading USER data GETUSER(). It was working with EF Core method for loading LISTING GetListing() because i had an include for photos Include(p => p.Photos).
After adding .ThenInclude(p => p.Photos) in GetUser(), the photos were returned with USER data and automapper successfully mapped User data and ListingPhotosUrl manual mapping was applied successfully.
Entity Framework Core DbContext:
public async Task<User> GetUser(int id)
{
var user = await _context.Users
.Include(a => a.Avatar)
.Include(l => l.MyListings)
.ThenInclude(p => p.Photos)
.FirstOrDefaultAsync(u => u.Id == id);
return user;
}
public async Task<Listing> GetListing(int id)
{
var listing = await _context.Listings
.Include(p => p.Photos)
.FirstOrDefaultAsync(l => l.id == id);
return listing;
}

Global/Shared mapping configuration: custom mappings & ignores

I'm working with .NET Core 2.0 and AutoMapper 6.2.2.
I'm using AssertConfigurationIsValid to make sure my mappings are correct and I have defined a number of AutoMapper.Profile which share some mapping rules:
Since I'm mapping between model entities and DTOs, I want field id
to be ignored in every entity <=> DTO mapping.
I also want AutoGeneratedAppid to be mapped to AppId in every
entity <=> DTO mapping.
For instance:
public class Role
{
public long Id { get; set; }
public string AutoGeneratedAppId { get; set; }
public string Name { get; set; }
}
public class RoleDTO
{
public string AppId { get; set; }
public string Name { get; set; }
}
public class RoleProfile : AutoMapper.Profile
{
public RoleProfile()
{
CreateMap<Role, RoleDTO>();
CreateMap<RoleDTO, Role>();
//.ForMember(entity => entity.Id, opt => opt.Ignore())
//.ForMember(entity => entity.AutoGeneratedAppId, opt => opt.MapFrom(dto => dto.AppId));
}
}
Since there both multiple mappings and multiple fields to be ignored/mapped in the same way in each mapping, I'm trying to found a way of avoiding having to define in every profile common rules this like:
.ForMember(entity => entity.Id, opt => opt.Ignore());
.ForMember(entity => entity.AutoGeneratedAppId, opt => opt.MapFrom(dto => dto.AppId));
I can't find a way to avoid a AutoMapperConfigurationException in AssertConfigurationIsValid when mapping dto => entity due to unmapped id and AutoGeneratedAppId properties.
My mapper configuration, with my failed trials, is the following:
protected override MapperConfiguration CreateConfiguration()
{
var config = new MapperConfiguration(cfg =>
{
cfg.DisableConstructorMapping();
cfg.AddProfiles(Assemblies);
// Global ignoring - alternative 1
cfg.ShouldMapProperty = prop =>
prop.Name != "Id";
// Global ignoring - alternative 2
cfg.AddGlobalIgnore("Id");
// Global ignoring - alternative 3
cfg.ForAllPropertyMaps(map =>
map.SourceMember.Name.EndsWith("Id"),
(map, configuration) =>
{
configuration.Ignore();
});
// Entity.AutoGeneratedAppId => DTO.AppId
cfg.RecognizePrefixes("AutoGenerated");
// DTO.AppId => Entity.AutoGeneratedAppId
cfg.RecognizeDestinationPrefixes("AutoGenerated");
});
return config;
}
My Startup configuration is the following:
var mapperProvider = new MapperProvider();
services.AddSingleton<IMapper>(mapperProvider.GetMapper());
services.AddAutoMapper(mapperProvider.Assemblies);
Thanks in advance.

Categories