How to use Automapper to flatten list of entity hierarchies?

automapper c# entity-framework entity-framework-core

Question

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.

1
1
11/28/2018 9:33:06 AM

Accepted Answer

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.

4
11/28/2018 9:30:13 AM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow