Nested collection mapping fails with automapper - c#

Somehow the mapping fails with the error message:
One or more errors occurred. (The following member on Data.Shop cannot be mapped:
Images
Add a custom mapping expression, ignore, add a custom resolver, or modify the destination type Data.Shop.
After having spent some time tinkering with it and searching for a solution on the official documentation + SO, I feel like I either messed up / forgot with a particular call in the profile, or need to resort to creating a custom map.. What do you guys think?
Configuration code that I used:
Entities
public class Shop
{
public Guid Id { get; set; }
public string Name { get; set; }
public Specialty Specialty { get; set; } = new Specialty();
public Address Address { get; set; } = new Address();
public string Owner { get; set; }
public List<Image> Images { get; set; } = new List<Image>();
public List<Tag> Tags { get; set; } = new List<Tag>();
public List<Promotion> Promotions { get; set; } = new List<Promotion>();
}
public class Image
{
public Guid Id { get; set; }
public string Title { get; set; }
public byte[] ImageData { get; set; }
}
DTOs
public class ShopDto
{
[Required]
public Guid Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string Specialty { get; set; }
[Required]
public int HouseNumber { get; set; }
[Required]
public string Street { get; set; }
[Required]
public string City { get; set; }
[Required]
public short Plz { get; set; }
[Required]
public string Owner { get; set; }
public List<ImageDto> Images { get; set; } = new List<ImageDto>();
public List<TagDto> Tags { get; set; } = new List<TagDto>();
public List<PromotionDto> Promotions { get; set; } = new List<PromotionDto>();
}
public class ImageDto
{
[Required]
public Guid Id { get; set; }
[Required]
public string Title { get; set; }
[Required]
public byte[] ImageData { get; set; }
}
Profile code
CreateMap<Image, ImageDto>();
CreateMap<Shop, ShopDto>()
.ForMember(dest => dest.City, member => member.MapFrom(source => source.Address.City))
.ForMember(dest => dest.Plz, member => member.MapFrom(source => source.Address.Plz))
.ForMember(dest => dest.Street, member => member.MapFrom(source => source.Address.Street))
.ForMember(dest => dest.HouseNumber, member => member.MapFrom(source => source.Address.HouseNumber))
.ForMember(dest => dest.Images, opt => opt.MapFrom(source => source.Images))
.ReverseMap()
.ForMember(src => src.Id, dest => dest.Ignore())
.ForMember(src => src.Promotions, dest => dest.MapFrom(src => src.Promotions))
.ForMember(src => src.Tags, dest => dest.MapFrom(src => src.Tags))
.ForMember(src => src.Images, dest => dest.MapFrom(src => src.Images))
.ForPath(src => src.Specialty.Name, dest => dest.MapFrom(src => src.Specialty))
.ForPath(src => src.Address.City, dest => dest.MapFrom(src => src.City))
.ForPath(src => src.Address.Plz, dest => dest.MapFrom(src => src.Plz))
.ForPath(src => src.Address.Street, dest => dest.MapFrom(src => src.Street))
.ForPath(src => src.Address.HouseNumber, dest => dest.MapFrom(src => src.HouseNumber));

You have defined a mapping from Shop to ShopDto but appear to be attempting to map ShopDto to Shop. You need define mappings in both directions or swap the template arguments. Of course you also need a mapping from ImageDto to Image.
Additionally you can remove this part:
.ForMember(src => src.Images, dest => dest.MapFrom(src => src.Images))
AutoMapper is smart enough to map collections if it has a mapping for the collection element type.

Related

Automapper map nested different object properties

I have the following Company and its nested object CompanyEmployee:
public class Company
{
public string Id { get; set; }
public string LogoPath { get; set; }
public string RammeId { get; set; }
public List<CompanyEmployee> Employees { get; set; }
}
public class CompanyEmployee
{
public string Id { get; set; }
[ForeignKey("Company")]
public string CompanyId { get; set; }
public Company Company { get; set; }
public string EmployeeId { get; set; }
}
Now I want to map the Entities to Dtos defined as the following objects CompanyDto and its nested object EmployeeDto:
public class CompanyDto
{
[Required]
public string Id { get; set; }
[Required]
public string Name { get; set; }
public string LogoPath { get; set; }
public string RammeId { get; set; }
public IFormFile FormFile { get; set; }
public List<EmployeeDto> Employees { get; set; }
}
public class EmployeeDto
{
public string Id { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
public string PhoneNumber { get; set; }
public List<RoleDto> Roles { get; set; }
}
My problem is the CompanyEmployee to EmployeeDto mapping.
How can I create a map that can take the property EmployeeId and map it to the Id property of EmployeeDto?
Currently, I have the following maps:
CreateMap<EmployeeDto, CompanyEmployee>(MemberList.Destination)
.ForMember(emp => emp.EmployeeId, opt => opt.MapFrom(ce => ce.Id));
CreateMap<CompanyDto, Company>(MemberList.Destination)
.ForMember(c => c.Employees.Select(e => e.CompanyId), opt => opt.MapFrom(cd => cd.Id));
CreateMap<Company, CompanyDto>(MemberList.Destination)
.ForMember(c => c.Id, opt => opt.MapFrom(cd => cd.Employees.First().CompanyId));
You want to create an AutoMapper Profile to configure each property mapping.
Create classes that inherit from Profile and put the configuration in the constructor.
For example:
public class EmployeeProfile : Profile
{
//Constructor
public EmployeeProfile()
{
//Mapping properties from CompanyEmployee to EmployeeDto
CreateMap<CompanyEmployee, EmployeeDto>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.EmployeeId));
//Mapping properties from EmployeeDto to CompanyEmployee
CreateMap<EmployeeDto, CompanyEmployee>()
.ForMember(dest => dest.EmployeeId, opt => opt.MapFrom(src => src.Id));
}
}
public class CompanyProfile : Profile
{
//Constructor
public CompanyProfile()
{
//Mapping properties from Company to CompanyDto
CreateMap<Company, CompanyDto>()
.ForMember(dest => dest.Employees, opt => opt.MapFrom(src => src.Employees));
//Mapping properties from CompanyDto to Company
CreateMap<CompanyDto, Company>()
.ForMember(dest => dest.Employees, opt => opt.MapFrom(src => src.Employees))
//Setting CompanyId
.AfterMap((src, dest) => {
foreach (var employee in dest.Employees)
{
employee.CompanyId = dest.Id;
}
});
}
}
AutoMapper Profile Configuration Documentation
Just create Profile and all properties which have the same name will mapped automatically. However, properties which do not have the same names, they should have custom mapping:
public class FromModelToDto : Profile
{
public FromModelToDto ()
{
CreateMap<CompanyEmployee, EmployeeDto>()
.ForMember(dest.Id, opts => opts.MapFrom(model => model.EmployeeId))
}
}
UPDATE:
If you want to map from Dto to Model, then you should create another mapping class:
public class FromDtoToModel : Profile
{
public FromDtoToModel ()
{
CreateMap<EmployeeDto, CompanyEmployee>()
.ForMember(dest.EmployeeId, opts => opts.MapFrom(model => model.Id))
}
}
You can read more about Automapper here.

mapping sub field issue

I have problem when mapping my entities to my models.
entity class 1
enter public partial class Contact
{
public int Id { get; set; }
public string Name { get; set; }
public int? CityId { get; set; }
public virtual City City { get; set; }
}
class 2
public partial class City
{
public City()
{
Contact = new HashSet<Contact>();
}
public int ID { get; set; }
public string Name { get; set; }
public virtual ICollection<Contact> Contact { get; set; }
}
The model for my contact is
public class ContactViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public KeyValueGeneric<int?, string> City = new KeyValueGeneric<int?, string>();
}
I'm trying to map it like this:
CreateMap<ContactViewModel, Contact>()
.ForMember(dest => dest.CityId, opts => opts.MapFrom(src => src.City.Id));
CreateMap<Contact, ContactViewModel>()
.ForMember(dest => dest.City.Id, input => input.MapFrom(src => src.CityId))
.ForMember(dest => dest.City.Value, input => input.MapFrom(src => src.City.Name));
but I'm getting:
must resolve to top-level member and not any child object message
I'm gonna appreciate any help
Assume it is because you are mapping to child properties:
dest.City.Id
Why not create a corresponding model for City (e.g. CityViewModel) and map to it directly?
CreateMap<City, CityViewModel>();
CreateMap<Contact, ContactViewModel>()
.ForMember(dest => dest.City, input => input.MapFrom(src => src.City));
If you do want to map to child properties, you may need to use ForPath() instead of ForMember(). E.g:
.ForPath(dest => dest.City.Id, input => input.MapFrom(src => src.CityId))

Automapper: Flattening by properties naming convention does not work

I want to flatten my data structure to dto.
My source class (simplified) looks like:
public class DeliveryNote
{
public DeliveryNoteNested DeliveryNoteNestedInstance { get; set; }
public string VehicleNo { get; set; }
}
public class DeliveryNoteNested
{
public string No { get; set; }
public string PlantNo { get; set; }
}
My dto (simplified too) like
public class DeliveryNoteDto
{
public int Id { get; set; }
public string No { get; set; }
public string PlantNo { get; set; }
public string VehicleNo { get; set; }
}
And then I do my mapping:
Mapper.Initialize(cfg => cfg.CreateMap<DeliveryNote, DeliveryNoteDto>());
var source = new DeliveryNote
{
VehicleNo = "VehicleNo20",
DeliveryNoteNestedInstance = new DeliveryNoteNested
{
No = "42",
PlantNo = "PlantNo10"
}
};
var dto = Mapper.Map<DeliveryNoteDto>(source);
At the end I expecting my properties No and PlantNo are filled in the dto by naming convention, but they are not.
When I do
Mapper.Initialize(cfg => cfg.CreateMap<DeliveryNote, DeliveryNoteDto>()
.ForMember(dest => dest.No, opt => opt.MapFrom(src => src.DeliveryNoteNestedInstance.No))
.ForMember(dest => dest.PlantNo, opt => opt.MapFrom(src => src.DeliveryNoteNestedInstance.PlantNo)));
it works, but in my real class I have close to 50 properties and I would like to avoid such boiler plate code when possible.
The basic convention would be
public class DeliveryNoteDto
{
public int Id { get; set; }
public string DeliveryNoteNestedInstanceNo { get; set; }
public string DeliveryNoteNestedInstancePlantNo { get; set; }
public string VehicleNo { get; set; }
}
You can also use
CreateMap(typeof(DeliveryNote), typeof(DeliveryNoteDto))
.AfterMap((s, d) => Mapper.Map(s.DeliveryNoteNested, d));

How to use Automapper to flatten list of entity hierarchies?

I want to use automapper to flatten a list of entity heirarchies returned back from Entity Framework Core.
Here are my entities:
public class Employee {
public int Id { get; set; }
[Required]
public string Name { get; set; }
public double? PayRateRegular { get; set; }
public double? PayRateLoadedRegular { get; set; }
public double? GMOutput { get; set; }
public string EmployeeType { get; set; }
//List of CommissionDetails where this employee is the consultant
public IEnumerable<CommissionDetail> CommissionDetailConsultants { get; set; } = new List<CommissionDetail>();
}
public class Project {
public int Id { get; set; }
public string Description { get; set; }
public double? BillRateRegular { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public Customer Customer { get; set; }
public int CustomerId { get; set; }
}
public class Customer {
public int Id { get; set; }
public string Name { get; set; }
}
public class CommissionDetail {
public string SaleType { get; set; }
public double CommissionPercent { get; set; }
public bool? IsReported { get; set; }
public int? Level { get; set; }
public string BasedOn { get; set; }
public Project Project { get; set; }
public int ProjectId { get; set; }
public Employee SalesPerson { get; set; }
public int SalesPersonEmployeeId { get; set; }
public Employee Consultant { get; set; }
public int ConsultantEmployeeId { get; set; }
}
Here is my DTO:
public class ConsultantGridViewModel
{
public string ConsultantName { get; set; }
public string CustomerName { get; set; }
public string SalesPersonName { get; set; }
public string ProjectDescription { get; set; }
public double? PayRate { get; set; }
public double? LoadedRated { get; set; }
public double? BillRate { get; set; }
public double? GM { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public double CommissionPercent { get; set; }
public int? CommissionLevel { get; set; }
}
Here is my call to EF:
return await _dbContext.Employee
.AsNoTracking()
.Include(e => e.CommissionDetailConsultants)
.ThenInclude(cd => cd.SalesPerson)
.Include(e => e.CommissionDetailConsultants)
.ThenInclude(cd => cd.Project)
.ThenInclude(p => p.Customer)
.Where(e => e.EmployeeType == "Contractor")
.ToListAsync();
I'm currently flattening it with SelectMany as follows:
var consultants = employees.SelectMany(e =>
e.CommissionDetailConsultants,
(emp, com) => new ConsultantGridViewModel {
ConsultantName = emp.Name,
PayRate = emp.PayRateRegular,
LoadedRated = emp.PayRateLoadedRegular,
GM = emp.GMOutput,
BillRate = com.Project.BillRateRegular,
ProjectDescription = com.Project.Description,
ProjectStartDate = com.Project.StartDate,
ProjectEndDate = com.Project.EndDate,
CustomerName = com.Project.Customer.Name,
SalesPersonName = com.SalesPerson.Name,
CommissionPercent = com.CommissionPercent,
CommissionLevel = com.Level
});
I would like to use automapper instead. I've used automapper for all my other DTO mappings but I can't figure out how to use it to flatten a nested object like this.
Let rewrite what you have currently with SelectMany + Select utilizing the Consultant navigation property:
var consultants = employees
.SelectMany(e => e.CommissionDetailConsultants)
.Select(com => new ConsultantGridViewModel
{
ConsultantName = com.Consultant.Name,
PayRate = com.Consultant.PayRateRegular,
LoadedRated = com.Consultant.PayRateLoadedRegular,
GM = com.Consultant.GMOutput,
BillRate = com.Project.BillRateRegular,
ProjectDescription = com.Project.Description,
ProjectStartDate = com.Project.StartDate,
ProjectEndDate = com.Project.EndDate,
CustomerName = com.Project.Customer.Name,
SalesPersonName = com.SalesPerson.Name,
CommissionPercent = com.CommissionPercent,
CommissionLevel = com.Level
});
Now it can be seen that the CommissionDetail contains all the necessary data, so while you can't avoid SelectMany, you can replace the Select by creating a mapping from CommissionDetail to ConsultantGridViewModel and use something like this:
var consultants = Mapper.Map<List<ConsultantGridViewModel>>(
employees.SelectMany(e => e.CommissionDetailConsultants));
or even better, project directly to the DTO:
var consultants = await _dbContext.Employee
.Where(e => e.EmployeeType == "Contractor")
.SelectMany(e => e.CommissionDetailConsultants)
.ProjectTo<ConsultantGridViewModel>()
.ToListAsync();
Now the mapping.
AutoMapper will map automatically members like CommisionPercent. Also the Flattening feature will handle automatically mappings like Project.EndDate -> ProjectEndDate, Consultant.Name -> ConsultantName etc.
So as usual with AutoMapper you should specify manually the mapping of properties which don't fall into previous categories. The minimal configuration in this case would be something like this:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<CommissionDetail, ConsultantGridViewModel>()
.ForMember(dst => dst.PayRate, opt => opt.MapFrom(src => src.Consultant.PayRateRegular))
.ForMember(dst => dst.LoadedRated, opt => opt.MapFrom(src => src.Consultant.PayRateLoadedRegular))
.ForMember(dst => dst.GM, opt => opt.MapFrom(src => src.Consultant.GMOutput))
.ForMember(dst => dst.BillRate, opt => opt.MapFrom(src => src.Project.BillRateRegular))
.ForMember(dst => dst.CustomerName, opt => opt.MapFrom(src => src.Project.Customer.Name))
.ForMember(dst => dst.CommissionLevel, opt => opt.MapFrom(src => src.Level));
});
P.S. You can even avoid SelectMany by basing your queries directly on CommissionDetail entity, for instance
var consultants = await _dbContext.Set<CommissionDetail>()
.Where(c => c.Consultant.EmployeeType == "Contractor")
.ProjectTo<ConsultantGridViewModel>()
.ToListAsync();
Note that when you do direct projection, there is no need of AsNoTracking or Include / ThenInclude.

AutoMapper EF with interfaces and nested collection

Here is my settings, after them goes error and description.
Interfaces:
public interface IUser
{
int Id { get; set; }
string Firstname { get; set; }
string Lastname { get; set; }
string Email { get; set; }
string Fullname { get; }
}
public interface IPost
{
int Id { get; set; }
String Description { get; set; }
DateTime CreatedDate { get; set; }
int? UserId { get; set; }
IUser User { get; set; }
ICollection<IComment> Comments { get; set; }
}
public interface IComment
{
int Id { get; set; }
string Description { get; set; }
DateTime CreatedDate { get; set; }
int? PostId { get; set; }
int? UserId { get; set; }
IUser User { get; set; }
}
AutoMapper config:
Mapper.Initialize(cfg => cfg.CreateMap<IUser, DAL.User>());
Mapper.Initialize(cfg => cfg.CreateMap<IComment, DAL.Comment>());
Mapper.Initialize(cfg => cfg.CreateMap<IPost, DAL.Post>()
.ForMember(d => d.User, m => m.MapFrom(s => s.User))
.ForMember(d => d.UserId, m => m.MapFrom(s => s.User.Id))
.ForMember(d => d.Comments, m => m.MapFrom(s => s.Comments))
);
Mapper.AssertConfigurationIsValid();
EF code:
public IEnumerable<IPost> GetPosts()
{
IQueryable<DAL.Post> query = dbSet.Posts.Include("Comments");
var result = Mapper.Map<IEnumerable<IPost>>(query);
return result;
}
Error:
{"The following property on HiThere.DAL.User cannot be mapped: \n\tUser \nAdd a custom mapping expression, ignore, add a custom resolver, or modify the destination type HiThere.DAL.User.\nContext:\n\tMapping to property User from HiThere.Model.Common.IUser to HiThere.DAL.User\nException of type 'AutoMapper.AutoMapperConfigurationException' was thrown."}
Description:
I'm using AutoMapper inside ASP.NET WebAPI2 project to map data between DAL and Models. For some reason I cannot make proper mapping when using nested objects.
Any help will be appreciated.
Actually I fix it by adding all cfg.CreateMap to single Mapper.Inizialize. My mistake. Hopefully someone will find this usefull.
Mapper.Initialize(cfg =>
{
cfg.CreateMap<DAL.Post, IPost>()
.ForMember(d => d.User, m => m.MapFrom(s => s.User))
.ForMember(d => d.UserId, m => m.MapFrom(s => s.User.Id))
.ForMember(d => d.Comments, m => m.MapFrom(s => s.Comments))
;
cfg.CreateMap<IUser, DAL.User>().ReverseMap();
cfg.CreateMap<IComment, DAL.Comment>().ReverseMap();
}
);
Mapper.AssertConfigurationIsValid();

Categories

Resources