How to use Automapper to flatten list of entity hierarchies? - c#

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.

Related

Nested collection mapping fails with automapper

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.

Entity Framework get SUM from child property

I have the following model where I'd like to get the sum of all OrderTotalItems for all Orders of a Customer where the OrderTotalType (Enumeration) is "total" or 99:
public class Customer
{
...
public ICollection<Order> Orders { get; set; } = new Collection<Order>();
}
public class Order
{
...
public ICollection<OrderTotalItem> OrderTotalItems { get; set; } = new Collection<OrderTotalItem>();
}
public class OrderTotalItem
{
[Required]
public int Id { get; set; }
[Required]
[Column(TypeName = "decimal(10, 4)")]
public decimal Value { get; set; }
[Required]
public OrderTotalType Type { get; set; }
}
I am building a CustomerAdminDTO to include all relevant data of a customer for the admin client:
public class CustomerAdminDto
{
public int Id { get; set; }
public string UserId { get; set; }
public Gender Gender { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string VATId { get; set; } = "";
public bool VATIdValid { get; set; } = false;
public DateTime Added { get; set; }
public DateTime LastModified { get; set; }
public decimal OrdersTotal { get; set; }
public CustomerStatusShortDto CustomerStatus { get; set; }
public CustomerAddressDto CustomerAddress { get; set; }
public CustomerAddressDto BillingAddress { get; set; }
public ICollection<OrderListShortDto> Orders { get; set; }
}
In my data service I fill the DTO like that
var customerAdmin = await _context.Customers
.Include(x => x.Addresses)
.Include(x => x.CustomerStatus)
.Include(x => x.Orders)
.ThenInclude(x => x.OrderTotalItems)
.Where(x => x.UserId == userid)
.Select(customer => new CustomerAdminDto
{
Id = customer.Id,
UserId = customer.UserId,
Gender = customer.Gender,
FirstName = customer.FirstName,
LastName = customer.LastName,
VATId = customer.VATId,
VATIdValid = customer.VATIdValid,
Added = customer.Added,
LastModified = customer.LastModified,
OrdersTotal = customer.Orders.Sum(x => x.OrderTotalItems
.Where(x => x.Type == Enums.OrderTotalType.Total)
.Sum(x => x.Value)),
CustomerStatus = new CustomerStatusShortDto
{
Id = customer.CustomerStatus.Id,
Name = customer.CustomerStatus.Name,
},
...
}
.FirstOrDefaultAsync();
where everything works, except the OrdersTotal.
API compiles fine but throws the following error at runtime:
Microsoft.Data.SqlClient.SqlException (0x80131904): Cannot perform an aggregate function on an expression containing an aggregate or a subquery.
Thanks for your hints!
Cannot perform an aggregate function on an expression containing an aggregate or a subquery.
This error in SQL server means that you tried to call aggregation function (customer.Orders.Sum() in your case) on other expression that contains aggregation function (.Sum(x => x.Value) in your case). In order to avoid this you can simplify your LINQ expression for OrdersTotal:
OrdersTotal = customer.Orders.SelectMany(o => o.OrderTotalItems).Where(x => x.Type == Enums.OrderTotalType.Total).Sum(x => x.Value)
There is only one aggregation here so it should work fine

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

Map variable from DTO class using LINQ

I have two classes called Participant and Screen.
public class Participant
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public ICollection<Screen> Screens { get; set; }
}
public class Screen
{
public int Id { get; set; }
public DateTime? SignedDateTime { get; set; }
}
And I use a DTO as below:
public class ParticipantForDashboardDto
{
public int Id { get; set; }
public string FirstName { get; set; }
public string MiddleName { get; set; }
public string LastName { get; set; }
public DateTime? ScreenDateTime { get; set; }
public ICollection<Screen> Screens { get; set; }
}
In here, since I don't have ScreenDateTime field in my Participant class, I decided to get this field from Screen class using AutoMapper. Here is my MappingProfile:
CreateMap<Participant, ParticipantForDashboardDto>()
.ForMember(dest => dest.ScreenDateTime, opt => {
opt.MapFrom(src => src.Screens.Select(x => x.SignedDateTime));
});
This returns 0001-01-01T00:00:00. I suspect that the SELECT query is not the one I should use. How can I map SignedDateTime from Screen class to ScreenDate in the dto?
Your currently using a queryable, which will return multiple result, you probably on need one, so you need to create a way to resolve the proper one, e.g sort and first or default:
CreateMap<Participant, ParticipantForDashboardDto>()
.ForMember(dest => dest.ScreenDateTime, opt => {
opt.MapFrom(src => src.Screens.Select(x => x.SignedDateTime)
.OrderByDescending(x => x)
.FirstOrDefault());
});

Populate multiple collections on same type

I need to populate a Product object which contains two collections.
The current code works fine and populates the Product.GraphicItems collection, but I also need to populate the Product.InfoItems collection, but I can't figure out the syntax for multiple collections.
Current select:
var result = await this.Context.ShopCategorys
.Include(cat => cat.InfoItems)
.Include(cat => cat.Products)
.ThenInclude(prd => prd.GraphicItems)
.ThenInclude(itm => itm.Graphic)
.ThenInclude(gfx => gfx.Items)
.SingleAsync(cat => cat.Id.Equals(id));
Product.cs:
[Table("ShopProduct")]
public class Product : BaseShop
{
public bool Active { get; set; } = true;
public int CategoryId { get; set; }
public int CultureId { get; set; } = -1;
public List<ProductInfo> InfoItems { get; set; } = new List<ProductInfo>();
public List<ProductGraphic> GraphicItems { get; set; } = new List<ProductGraphic>();
}
ProductInfo.cs:
[Table("ShopProductInfo")]
public class ProductInfo : BaseShop, IInfoItem
{
public int? ProductId { get; set; }
public int CultureId { get; set; }
public Culture Culture { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
Solution:
var result = await this.Context.ShopCategorys
.Include(cat => cat.InfoItems)
.Include(cat => cat.Products)
.ThenInclude(prd => prd.InfoItems)
.Include(cat => cat.Products)
.ThenInclude(prd => prd.GraphicItems)
.ThenInclude(itm => itm.Graphic)
.ThenInclude(gfx => gfx.Items)
.SingleAsync(cat => cat.Id.Equals(id));

Categories

Resources