Search code examples
asp.net-coreentity-framework-coreautomapper

How to use Automapper to ProjectTo child entity


I have the following entity classes

public class Customer
{
    public long Id { get; set; }
    [StringLength(100)]
    public string Name { get; set; } = string.Empty;
    [StringLength(254)]
    public string? Email { get; set; }
    public List<Order> Orders { get; set; } = new List<Order>();
}

public class Order
{
    public long Id { get; set; }
    public long CustomerId { get; set; }
    [ForeignKey(nameof(CustomerId))]
    public Customer? Customer { get; set; }
    public DateOnly Date { get; set; }
    public List<OrderLine> Lines { get; set; } = [];
}

public class OrderLine
{
    public long Id { get; set; }
    public long OrderId { get; set; }
    [ForeignKey(nameof(OrderId))]
    public Order? Order { get; set; }
    [StringLength(100)]
    public string Item { get; set; } = string.Empty;
    public decimal Amount { get; set; }
}

And, there are corresponding data transfer objects (DTO):

public class CustomerDto
{
    public long Id { get; set; }
    public string Name { get; set; } = string.Empty;
}

public class OrderDto
{
    public long Id { get; set; }
    public long CustomerId { get; set; }
    public CustomerDto? Customer { get; set; }
    public DateOnly Date { get; set; }
    public List<OrderLineDto> Lines { get; set; } = [];
}

public class OrderLineDto
{
    public long Id { get; set; }
    public long OrderId { get; set; }
    public string Item { get; set; } = string.Empty;
    public decimal Amount { get; set; }
}

I made the following AutoMapper configuration:

public class AutoMapperProfile : Profile
{
    public AutoMapperProfile()
    {
        CreateMap<Customer, CustomerDto>()
            .ReverseMap()
            .ForPath(o => o.Id, o => o.Ignore());
        CreateMap<Order, OrderDto>()
            .ReverseMap()
            .ForPath(o => o.Id, o => o.Ignore());
        CreateMap<OrderLine, OrderLineDto>()
            .ReverseMap()
            .ForPath(o => o.Id, o => o.Ignore());
    }
}

The above configuration is registered to DI of my ASP.NET web application:

services.AddAutoMapper(typeof(AutoMapperProfile));

Now, I'd like to search Orders entity with specific search criteria and map it to OrderDto and send to my web client. For that I do the following:

var config = new MapperConfiguration(cfg => cfg.CreateProjection<Order, OrderDto>());
IQueryable<Order> query = db.Orders
    .Include(o => o.Customer)
    .Include(o => o.Lines);

// Apply some Where criteria here

var orderDtos = await query
    .AsNoTracking()
    .OrderBy(o => o.Id)
    .ProjectTo<OrderDto>(config)
    .ToListAsync();

However, I got the following error:

Unable to create a map expression from Order.Customer to CustomerDto.Customer ...

I guess similar issue exists for OrderLineDto even though error message doesn't tell anything about it yet. I can I solve the issue? It looks like I should edit my configuration with ForMember method, but I couldn't make it work.


Solution

  • The initialization of your mapping config is a bit suspect. If you are using an Automapper Profile, try this:

    var config = new MapperConfiguration(cfg => cfg.AddProfile(new AutoMapperProfile());
    var orderDtos = await db.Orders
        .OrderBy(o => o.Id)
        .ProjectTo<OrderDto>(config)
        .ToListAsync();
    

    Note that when using projection you do not need to eager load related entities (Include) or worry about tracking. (AsNoTracking). No entities are populated or tracked, and EF will join the necessary tables automatically based on the mapped projections.