0

I have three objects:

  1. Event (relationships: has many EventProducts)

  2. Product (relationships: has many EventProducts)

  3. EventProduct (relationships: with many Events, with many Products, has properties like quantity_sold, quantity_allocated, etc which should be stored per product and per event)

In my application, when someone clicks an Event, a new window initializes with a list of all products. From there they can modify cells I need to populate a datagrid in WPF such that when someone clicks an event. So in the context of this code, the Event is known and constant.

The issue I'm having is with creating default types of EventProduct.

What I need is to have a query where if there is no EventProduct in the DB, it will instantiate an EventProduct with EventProduct.Event = currentEvent (currentEvent will be constant for all EventProducts created with this query) and EventProduct.Product = product (product will change for each row)

This code works well when there is an associated EventProduct in the database. But if there isn't, my selection returns my Product just fine but the entire EventProduct is null.

var query2 = from product in dbContext.Products
             join eventProduct in dbContext.EventProducts
                 on new { pIndex = product.index, eIndex = currentEvent.index }
                 equals new { pIndex = eventProduct.Product.index, eIndex = eventProduct.Event.index } into temp
             from eventProduct in temp.DefaultIfEmpty() // this is my problem line
             select new { 
                    Product = product,
                    EventProduct = eventProduct
              };

I have tried creating a constructor for EventProduct(Event e, Product p) and passing in the values to the constructor in my DefaultIfEmpty() method but I get errors that my constructor has to have 0 arguments to be used in that way. I can't do that because there is no way to tell my EventProduct() object which Event and Product it should be associated with if I do that.

I have also tried no constructor, just creating a new EventProduct and setting its properties but I get the error "The entity or complex type ...EventProduct cannot be constructed in a LINQ to Entites query".

At the end result, I want to select my Product and EventProduct. If there is no EventProduct associated with both that Product and Event, then my EventProduct selection should be set to a default which has the currentEvent, the current row's Product, and all properties set to a default value (all are decimals and should be 0 in this case).

EDIT: I've just tried this query and it also gives me an Unsupported error:

var query2 = from product in dbContext.Products
             join eventProduct in dbContext.EventProducts
                 on new { pIndex = product.index, eIndex = currentEvent.index }
                 equals new { pIndex = eventProduct.Product.index, eIndex = eventProduct.Event.index } into temp
             from eventProduct in temp.DefaultIfEmpty()
             select new { 
                 Product = product,
                 EventProduct = eventProduct != null ?
                 eventProduct : new EventProduct
                 {
                     Product = product,
                     Event = currentEvent,
                     quantity_allocated = 0,
                     quantity_sold = 0,
                     quantity_sampled = 0
                 }
             };

EDIT: Resolved with this technique:

1) Create an object because anonymous objects are read-only:

    class Associations
    {
        public class ProductEventProduct
        {
            public Product Product { get; set; }
            public EventProduct EventProduct { get; set; }
        }
    }

2) Foreach null object in the dataset, replace with a default object

var query = from product in dbContext.Products
                         join eventProduct in dbContext.EventProducts
                             on new { pIndex = product.index, eIndex = currentEvent.index }
                                equals new { pIndex = eventProduct.Product.index, eIndex = eventProduct.Event.index } into temp
                         from eventProduct in temp.DefaultIfEmpty()
                         select new Associations.ProductEventProduct { 
                             Product = product,
                             EventProduct = eventProduct
                         };
            var dataSource = query.ToList();

            foreach (Associations.ProductEventProduct entry in dataSource)
            {
                if (entry.EventProduct == null)
                {
                    entry.EventProduct = new EventProduct
                    {
                        Product = entry.Product,
                        Event = currentEvent,
                        quantity_allocated = 0,
                        quantity_sold = 0,
                        quantity_sampled = 0
                    };
                }
            }
rook218
  • 644
  • 7
  • 20

2 Answers2

1

It appears that the issue you are having is that you are asking Entity Framework to create a query that it doesn't know how to create.

Keep in mind that Linq uses deferred execution, so when you write your query, it's just a placeholder of where and how to get the data rather than the actual data. It isn't until some other code asks for the data that the query actually runs and your information is populated.

For Linq to Entities, this means it will build up a query in the DB language and hold on to it until you need the data, then execute the query as you would expect through whatever DB provider you are using when you call for it, and store it in memory to work against.

So when you try to construct a new EventProduct in your query to fill the null as you have it, the DB doesn't know what that is and you get the error, since you are still in the part of the query where the DB is responsible for objects. The DB Provider doesn't know how to construct that new object, so EF can't translate the query and you get the error.

The solution is to "hydrate" your information before you try to construct the new EventProduct, by running your query sans EventProduct constructor through a call to ToList() or ToArray or similar, which forces the query to run right then. Then, AFTER the data is hydrated (in memory), go through all of the objects you created in the query and if the EventProduct is null, construct a new one and add it dynamically, however you want to do it. (A Second query with a Select statement maybe?) This should get you around the error you mentioned.

I'm a bit out of touch on if your original query, which produces anonymous objects, would be tracked by the DBContext or not, but keep that in mind as well and test for it. If you add a bunch of EventProduct instances that are tied to those anonymous objects, you could have those included by default in tracking, and a call to SaveChanges() might inadvertently write them all to the DB when you weren't intending to do so. Something to keep in mind.

Hope this helps, let me know!

Nik P
  • 2,693
  • 9
  • 21
  • thank you that makes a lot of sense. I think my approach is off for this project, I'm thinking too much in terms of SQL views/ stored procedures and not in the Entity Framework mindset. I don't mind populating the EventProduct dataset with blank data, but then it becomes a nightmare to maintain when there are 100 events and someone adds a product... Do you think the approach of having a relational object like EventProduct is the right way to go about it? – rook218 Jun 03 '20 at 23:22
  • 1/2 Having a table to represent data like this is very common. The question that comes to mind is what does the null value for EventProduct represent? From my extremely limited knowledge of your domain, it says that there is no product at a particular event and the values are all zero. In other words, absence still has a value, and you can use that information to write conditional logic in your datagrid to represent zero values. This would mean you could use the Event and Product tables to hold data and only build up an EventProduct and persist it when there is something of value to persist. – Nik P Jun 04 '20 at 04:09
  • 2/2 - This means you could add a new product and it shouldn't matter, because the null EventProduct that would appear in the DataGrid indicates a zero starting value, which is probably accurate. You can then change and add new EventProducts as needed. Products and Events become backing list information for the EventProduct, which is either the star of the show or doesn't exist. Hopefully that makes sense but let me know if it's confusing or way off base. – Nik P Jun 04 '20 at 04:13
  • thanks so much! I took your thoughts into consideration and have come up with a good solution to the code. I will have to have some logic on save, to add the appropriate objects to my database if they don't already exist, but I'm on the right track to make progress instead of staying stuck. Thanks! – rook218 Jun 04 '20 at 21:40
1

Have you tried setting default value in case of null in the select clause?

......
select new 
{ 
    Product = product,
    EventProduct = eventProduct != null 
        ? eventProduct 
        : new EventProduct { .... set default values}
}

UPD

There are possible workarounds to this problem, either introduce DTO (or another anonymous type) as this answer suggests:

select new 
{ 
    Product = product,
    EventProduct = new 
    {
         Product = product,
         EventId = currentEvent.Id, // possibly you will need to copy every field by hand 
         quantity_allocated = eventProduct == null ? 0 : eventProduct .quantity_allocated ,
         ....
    }
}

Or just replace nulls after the query to db cause you should have all the needed data already:

var query2 = from product in dbContext.Products
         join eventProduct in dbContext.EventProducts
             on new { pIndex = product.index, eIndex = currentEvent.index }
             equals new { pIndex = eventProduct.Product.index, eIndex = eventProduct.Event.index } into temp
         from eventProduct in temp.DefaultIfEmpty()
         select new { 
             Product = product,
             EventProduct = eventProduct
         };
var results = query2.ToList();
foreach(var r in results)
{
      r.EventProduct = r.EventProduct != null 
            ? r.EventProduct 
            : new EventProduct { .... set default values}
}
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • I don't believe this will work, since I would need to set different default values for each EventProduct. So if I set it up this way, how could I have an EventProduct for Product1, an EventProduct for Product2, etc? – rook218 Jun 03 '20 at 23:24
  • 1
    @rook218 you can use `product` when you are filling the `EventProduct` like : `new EventProduct { Product_index = product.index }` – Guru Stron Jun 03 '20 at 23:28
  • Thanks but that gives me the error, "'The entity or complex type '....EventProduct' cannot be constructed in a LINQ to Entities query." I've added the full query that I tried to the end of my question – rook218 Jun 04 '20 at 00:13
  • Thanks so much! I got the issue resolved with a slight modification of your code (in my answer). I was hoping I could just nab everything I needed in a query to keep my dbContext neat, but it looks like I'll just have to code around this issue. – rook218 Jun 04 '20 at 21:39
  • @rook218 was glad to help! – Guru Stron Jun 04 '20 at 21:42