Home > Enterprise >  EF Include Child Collection returning Nulls in list
EF Include Child Collection returning Nulls in list

Time:03-19

I am having an issue with EF returning NULL values within a child list. Here is my model that I am trying to get:

public class CompoundIngredient : Ingredient
{
  public List<MeasuredIngredient> MeasuredIngredients { get; set; }

  public string UserId { get; set; }          

  public CompoundIngredient()
  {
    MeasuredIngredients = new List<MeasuredIngredient>();
    IsPublic = true;
  }
}

However, when I do this:

return await _dataContext.CompoundIngredients
  .Include(a => a.MeasuredIngredients)
    .ThenInclude(a => a.MeasurementType)
  .Include(a => a.MeasuredIngredients)
    .ThenInclude(a => a.Ingredient)
    .ThenInclude(a => a.IngredientType)
  .FirstOrDefaultAsync(c => c.DisplayValue == name);

I get back a collection of 4 items. 2 items are populated and 2 are NULL.

enter image description here

Here is the data in the DB

enter image description here

As you can see from the picture there are 4 entries in the table, 2 of which belong to CompoundIngredientId 6 which is the ID of the ingredient who matches the Name value.

Why am I getting 4 results back, 2 of which are null?

EDIT:

So here are the models

public class CompoundIngredient : Ingredient
    {
        public List<MeasuredIngredient> MeasuredIngredients { get; set; }

        public string UserId { get; set; }      

        

        public CompoundIngredient()
        {
            MeasuredIngredients = new List<MeasuredIngredient>();
            IsPublic = true;
        }
    }
public class Ingredient
    {
        public int Id { get; set; }
        public string DisplayValue { get; set; }
        public string Description { get; set; }
        public bool IsPublic { get; set; }


        public IngredientType IngredientType { get; set; }
        public int IngredientTypeId { get; set; }
        public int CompanyId { get; set; }
        public string UserName { get; set; }


        public string CreatedBy { get; set; }
        public DateTime CreatedDate { get; set; }
        public string LastModifiedBy { get; set; }
        public DateTime LastModifiedDate { get; set; }
    }
public class MeasuredIngredient 
    {
        public int Id { get; set; }

        public decimal Amount { get; set; }

        public int MeasurementTypeId { get; set; }
        public MeasurementType MeasurementType { get; set; }

        public int IngredientId { get; set; }
        public Ingredient Ingredient { get; set; }

        public int? UseId { get; set; }
        public Use Use { get; set; }
    }
public class Recipe
    {
        public int Id { get; set; }
        public string UserId { get; set; }

        public string Name { get; set; }
        public string Description { get; set; }
        public bool IsPublic { get; set; }
        

        public int RecipeCategoryId { get; set; }
        public RecipeCategory RecipeCategory { get; set; }
        public int SocialMediaId { get; set; }
        public SocialMedia SocialMedia { get; set; }
        
        public virtual List<TimeTemp> TimeTemps { get; set; }
        public virtual List<RecipeFuel> RecipeFuels{ get;set; }
        public List<MeasuredIngredient> MeasuredIngredients { get; set; }
        public List<RecipeStep> RecipeSteps { get; set; }

        
        public string CreatedBy { get; set; }
        public DateTime CreatedDate { get; set; }
        public string LastModifiedBy { get; set; }
        public DateTime LastModifiedDate { get; set; }

        public Recipe()
        {
            MeasuredIngredients = new List<MeasuredIngredient>();
            RecipeSteps = new List<RecipeStep>();
            SocialMedia = new SocialMedia();
            RecipeFuels = new List<RecipeFuel>();
            TimeTemps = new List<TimeTemp>();
            IsPublic = true;
        }
    }

As you can see Measured Ingredient isnt exclusive to CompoundIngredient. Recipe also has a List on it as well.

as far as configurations i dont have much

public class MeasuredIngredientConfiguration : IEntityTypeConfiguration<MeasuredIngredient>
    {
        public void Configure(EntityTypeBuilder<MeasuredIngredient> builder)
        {
            builder.Property(p => p.UseId).IsRequired(false);

        }
    }
public class IngredientConfiguration : IEntityTypeConfiguration<Ingredient>
    {
        public void Configure(EntityTypeBuilder<Ingredient> builder)
        {
            builder.Property(p => p.IsPublic).HasDefaultValue(true);
        }
    }
public class RecipeConfiguration : IEntityTypeConfiguration<Recipe>
    {
        public void Configure(EntityTypeBuilder<Recipe> builder)
        {
            builder.Property(p => p.IsPublic).HasDefaultValue(true);

        }
    }

here are the tables in the DB with FKs

enter image description here

enter image description here

CodePudding user response:

I highly suspect the issue will stem from CompoundIngredient inheriting from Ingredient using TPH inheritance (One table with a Discriminator) then being referenced by MeasuredIngredient, and this relationship not being set up quite right. Which version of EF Core is this?

Overall the relationship between these entities/tables feels "odd". You have an ingredient, then a "compound" ingredient that is made up of one or more MeasuredIngredient. (which does not extend ingredient) A Measured ingredient contains one Ingredient, and optionally one CompoundIngredient.

Given a compound ingredient represents just a collection of measured ingredients, this just feels a bit off. It sounds like you want a recipe to contain a list of ingredients where each is associated with a measurement (The MeasuredIngredient) but that "ingredient" may be a combination of other ingredients (with associated measurements) where you may want to possibly avoid duplicating data.

I built a simple test with the core relationships in EF Core 5 and I was able to get the expected results. The important detail here was ensuring the relationship between the 3 classes (and possibly other related classes) is configured correctly. For example, cutting down the object model down to the core I came up with:

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

public class CompoundIngredient : Ingredient
{
    public virtual ICollection<MeasuredIngredient> MeasuredIngredients { get; set; } = new List<MeasuredIngredient>();
}

public class MeasuredIngredient
{
    public int Id { get; set; }
    publi int IngredientId { get; set; }
    public virtual Ingredient Ingredient { get; set; }
}

Creating test records with the relationships you showed and running EF Core 5 I wasn't able to reproduce the issue, but I honestly did not feel comfortable with leaving EF to sort out the discriminator and relationships.

The bits I didn't like were: CompoundIngredient extends Ingredient while containing a Many relationship to MeasuredIngredient where there is no corresponding "One" relationship on MeasuredIngredient, but it does have a "One" relationship with Ingredient.

The discriminator here is implied, not configured.

What I am more comfortable with was:

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

public class CompoundIngredient : Ingredient
{
    public virtual ICollection<MeasuredIngredient> MeasuredIngredients { get; set; } = new List<MeasuredIngredient>();
}

public class MeasuredIngredient
{
    public int Id { get; set; }
    public virtual Ingredient Ingredient { get; set; }
    public virtual CompoundIngredient CompoundIngredient { get; set; }
}

Then explicitly mapping the relationships to ensure there is no confusion on FKs:

public class MeasuredIngredientConfiguration : IEntityTypeConfiguration<MeasuredIngredient>
{
    public void Configure(EntityTypeBuilder<MeasuredIngredient> builder)
    {
        builder.Property(p => p.UseId).IsRequired(false);
        builder.HasOne(p => p.Ingredient)
            .WithMany()
            .IsRequired()
            .HasForeignKey("IngredientId");
        builder.HasOne(p => p.CompoundIngredient)
            .WithMany(p => p.MeasuredIngredients)
            .IsRequired(false)
            .HasForeignKey("CompoundIngredientId");

    }
}
public class IngredientConfiguration : IEntityTypeConfiguration<Ingredient>
{
    public void Configure(EntityTypeBuilder<Ingredient> builder)
    {
        builder.Property(p => p.IsPublic).HasDefaultValue(true);
        builder.HasDiscriminator<string>("Discriminator")
            .HasValue<Ingredient>("I")
            .HasValue<CompoundIngredient>("C"); // Whichever discriminator values you want to use.
    }
}

I generally do not have FKs exposed in entities for navigation properties, opting for shadow properties. This should work just as well with the FK fields mapped.

Now I had excluded this configuration and this example did work with EF Core 5. I was also trying to force a misconfiguration around possibly the CompoundIngredientId and IngredientId in the measured ingredient, but outside of generating specific configuration errors around missing assumed FKs I wasn't able to reproduce your issue. It could also be behaviour specific to the version of EF Core you are using.

You could try adding the explicit mapping to see if that solves or otherwise changes your results. Getting null entries in your collection smells like EF is trying to parse the CompoundIngredient -> MeasuredIngredient, but it is getting other measured Ingredients with the same Ingredient reference (1-2) but not the matching compound ingredient ID. It's definitely a weird one.

Otherwise I would look to temporarily eliminate all other references such as Recipe, measurement type, etc. down to the simplest possible example and data set that reproduces the problem. This becomes easier to investigate options to identify where/what is getting mixed up.

Hopefully this gives you some ideas on how to get to the bottom of the issue.

CodePudding user response:

Turns out the issue is not with EF… after looking further into it EF is returning the proper counts and relations. The issue is higher up during the deserialization of the json being returned from the API. I created a new question for this

JsonSerializer.DeserializeAsync<> Creating Null Items in Collections

  • Related