Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Error with two structurally incompatible initializations within a single LINQ

I have a model which has is below and when I add ComboProducts object with the linq below I am getting the error. Is there something I am missing in this code?

Error

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.

Model

public class Product
{
    public int ProductId { get; set; }
    public string Name { get; set; }
    public List<Product> ComboProducts { get; set; }
}

Code

ListOfProducts = (
    from p in db.Products
    join ptp in db.ProductToProducts on p.ProductId equals ptp.ParentProductId
    join pr in db.Pricings on ptp.ProductToProductId equals pr.ProductToProductId
    where p.SubProductTypeId == (int)SubProductTypeId
    select new Model.Product
    {
        ProductId = p.ProductId,
        Name = p.Name,
        PreviewUrl = p.PreviewURL,
        ShortName = p.ShortName,
        Description = p.Description,
        PublicProduct = p.PublicProduct,
        Price = pr.Price,
        ComboProducts = (
            from ptpx in db.ProductToProducts
            join ap in db.Products on ptpx.ChildProductId equals ap.ProductId
            where ptpx.ParentProductId == p.ProductId
            select new Model.Product
            {
                Name = ap.Name,
                ProductId = ap.ProductId
            }
        ).OrderBy(p2 => p2.Name).ToList(),
        DisplayOrder = p.DisplayOrder != null ? (int)p.DisplayOrder : 0
    })
    .GroupBy(grp => new
        {
            grp.ProductId
        }
    )
    .Select(x => x.FirstOrDefault())
    .ToList();

return ListOfProducts;
like image 328
Jefferson Avatar asked Jul 23 '20 17:07

Jefferson


2 Answers

The exception message is clear, you have two select new NCCN.Model.Product statements in one LINQ statement that don't set the same properties in the same order. It's an EF limitation we have to deal with. LINQ-to-objects wouldn't throw this exception.

Although it's not quite clear from your question, I think I understand what you're actually asking. Even when understanding the exception message, it's not obvious how to deal with it. Before pointing out the problem, let me first introduce a simplified model.

public class Product
{
    public int ProductId { get; set; }
    public string Name { get; set; }
    
    public ICollection<ProductComponent> Components { get; set; }
}

public class ProductComponent
{
    public int ProductId { get; set; }
    public int ComponentProductId { get; set; }
    public Product Product { get; set; }
    public Product ComponentProduct { get; set; }
}

With mapping code.

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<ProductComponent>().HasKey(e => new { e.ProductId, e.ComponentProductId });
    modelBuilder.Entity<ProductComponent>().HasRequired(e => e.Product)
        .WithMany(p => p.Components).HasForeignKey(e => e.ProductId);
    modelBuilder.Entity<ProductComponent>().HasRequired(e => e.ComponentProduct)
        .WithMany().HasForeignKey(e => e.ComponentProductId)
        .WillCascadeOnDelete(false); // Prevent circular cascade
}

Now we can use a navigation property instead of joining all the time.

The task is to map this model to a DTO class:

public class ProductDto
{
    public int ProductId { get; set; }
    public string Name { get; set; }
    public List<ProductDto> ComboProducts { get; set; }
}

It should be as simple as

var products = db.Products.Select(p => new ProductDto
{
    ProductId = p.ProductId,
    Name = p.Name,
    ComboProducts =
        p.Components.Select(pc => pc.ComponentProduct)
         .Select(c => new ProductDto
         {
            ProductId = c.ProductId,
            Name = c.Name,
         }).ToList()
});

But now EF throws the exception you reported. In your case you even skipped a whole range of properties, here it's only ComboProducts in the nested ProductDto, but that's enough. The inner ProductDto should have a ComboProducts collection too.

Unfortunately, that's not a simple as one would expect. For example, with this inner select...

 .Select(c => new ProductDto
 {
    ProductId = c.ProductId,
    Name = c.Name,
    ComboProducts = null
 }).ToList()

...EF throws

NotSupportedException: Unable to create a null constant value of type 'System.Collections.Generic.List`1[[ProductDto]]'. Only entity types, enumeration types or primitive types are supported in this context.

And...

 .Select(c => new ProductDto
 {
    ProductId = c.ProductId,
    Name = c.Name,
    ComboProducts = new List<ProductDto>()
 }).ToList()

...throws

NotSupportedException: A type that implements IEnumerable 'System.Collections.Generic.List`1[[ProductDto]]' cannot be initialized in a LINQ to Entities query.

What it boils down to is: with such nested projections you have to use two different types for the main and nested types. But anonymous types also fit the bill, so I think the easiest way to work around this annoying EF limitation is to project to anonymous types and then to ProductDto:

var products = db.Products.Select(p => new
{
    ProductId = p.ProductId,
    Name = p.Name,
    ComboProducts =
        p.Components.Select(pc => pc.ComponentProduct)
         .Select(c => new
         {
            ProductId = c.ProductId,
            Name = c.Name
         }).ToList()
}).AsEnumerable() // Continue in memory so EF won't translate the following part
.Select(x => new ProductDto
{
    ProductId = x.ProductId,
    Name = x.Name,
    ComboProducts = x.ComboProducts.Select(cp => new ProductDto
    {
        Name = cp.Name,
        ProductId = cp.ProductId,
    }).ToList()
}
).ToList();
like image 71
Gert Arnold Avatar answered Nov 05 '22 09:11

Gert Arnold


Split the complex linq into small unit(it will be useful for maintainablity, readablity, and reduce complexity)

Try my suggestion:

in above example you have added nested linq. So move ComboProducts linq out of this linq. First calculate the Comboproducts and assign that value into var. then next build parent query on top of it.

like image 1
priya Avatar answered Nov 05 '22 11:11

priya