Mahdi
Mahdi

Reputation: 141

Automapper nested mappings along with projection

Regarding to the blogging data model I have:

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

public class Post
{
    public int Id { get; set; }
    public string Text { get; set; }
    public int BlogId { get; set; }

    public virtual Blog Blog { get; set; }
}

Also, corresponding DTOs are defined in this way:

public class BlogDto
{
    public int Id { get; set; }
    public string Name { get; set; }

    public ICollection<PostDto> Posts { get; set; }
}

public class PostDto
{
    public int Id { get; set; }
    public string Text { get; set; }
    public int BlogId { get; set; }

    public BlogDto Blog { get; set; }
}

Then, mapping is initialized:

Mapper.Initialize(cfg =>
        {
            cfg.CreateMap<Blog, BlogDto>();
            cfg.CreateMap<Post, PostDto>();
        });

Finally, I try to project Posts to PostDtos using Automapper.EF6:

List<PostDto> result = null;
using (var db = new BloggingContext())
{
    result = db.Set<Post>().ProjectToList<PostDto>();
}

But I encounter this error:

The type 'AutoMapperSample.Model.PostDto' appears in two structurally
incompatible initializations within a single LINQ to Entities query. A type
can be initialized in two places in the same query, but only if the same
properties are set in both places and those properties are set in the same
order.

This is the generated expression:

.Call System.Linq.Queryable.Select(
.Call 

.Constant<System.Data.Entity.Core.Objects.ObjectQuery`1[AutoMapperSample.Model.Post]>(System.Data.Entity.Core.Objects.ObjectQuery`1[AutoMapperSample.Model.Post]).MergeAs(.Constant<System.Data.Entity.Core.Objects.MergeOption>(AppendOnly))
    ,
    '(.Lambda #Lambda1<System.Func`2[AutoMapperSample.Model.Post,AutoMapperSample.Model.PostDto]>))

.Lambda #Lambda1<System.Func`2[AutoMapperSample.Model.Post,AutoMapperSample.Model.PostDto]>(AutoMapperSample.Model.Post $dto)
{
    .New AutoMapperSample.Model.PostDto(){
        Id = $dto.Id,
        Text = $dto.Text,
        Blog = .If ($dto.Blog != null) {
            .New AutoMapperSample.Model.BlogDto(){
                Id = ($dto.Blog).Id,
                Name = ($dto.Blog).Name,
                Posts = .Call System.Linq.Enumerable.ToList(.Call System.Linq.Enumerable.Select(
                        ($dto.Blog).Posts,
                        .Lambda #Lambda2<System.Func`2[AutoMapperSample.Model.Post,AutoMapperSample.Model.PostDto]>))
            }
        } .Else {
            null
        }
    }
}

.Lambda #Lambda2<System.Func`2[AutoMapperSample.Model.Post,AutoMapperSample.Model.PostDto]>(AutoMapperSample.Model.Post $dto)
{
    .New AutoMapperSample.Model.PostDto(){
        Id = $dto.Id,
        Text = $dto.Text
    }
}

I need to know if the structure is wrong such as loop, or something else need to be considered.

Upvotes: 1

Views: 733

Answers (1)

Yaser Moradi
Yaser Moradi

Reputation: 3337

EF 6's projection has two issues

1- You can't project Set to Customer itself.

For example

public class Customer
{
     public int Id {get;set;}
     public string Name {get;set;}
     [NotMapped]
     public int OrdersCount {get;set;}
     public List<Order> Orders {get;set;}
}

List<Customer> customers = await DbContext.Customers.Select(cust => 
     new Customer
     {
          Id = cust.Id,
          Name = cust.Name,
          OrdersCount = cust.Orders.Count
     }).ToListAsync(); // error!

2- You can't project both side of relationships at the same time


public class Customer // as like as previous example

public class Order
{
     public int Id {get;set;}
     public int CustomerId {get;set;}
     public Customer Customer {get;set;}
}

List<CustomerDto> customers = await DbContext.Customers.Select(cust => 
     new CustomerDto
     {
          Id = cust.Id,
          Name = cust.Name,
          OrdersCount = cust.Orders.Count,
          Orders = cust.Orders.Select(ord => new OrderDto {
               Id = ord.Id,
               CustomerId = ord.CustomerId,
               Customer = new CustomerDto { ... }
          }).ToList()
     }).ToListAsync(); // error!

First issue is not resolvable, but for the 2nd issue, you can write a workaround which ignores one side of the association (for example, returns customers with orders in customers query, but won't return customer in orders query!)

bool MapperPropConfigurationCondition(PropertyMap p)
            {
                return (p.DestinationMember.GetCustomAttribute<ForeignKeyAttribute>() != null || p.DestinationMember.GetCustomAttribute<InversePropertyAttribute>() != null)
                       && !typeof(IEnumerable).IsAssignableFrom(p.DestinationMember.ReflectedType)
                       && typeof(IDto).IsAssignableFrom(p.DestinationMember.ReflectedType);
            }

            mapperConfigExpression.ForAllPropertyMaps(MapperPropConfigurationCondition, (p, member) =>
            {
                p.Ignored = true;
            });

Final solution: Migrate to EF Core 5, it has everything you need and you're good to go! ;D

Upvotes: 1

Related Questions