-1

I am a little puzzled as to why this code doesn't work. I have a basic one-to-many relationship where I load a parent and include its children. I later I am trying to navigate from the child back to the parent, but the parent is null and I can't figure out why.

Question:

Why can't I query a graph of parent/child objects and navigate backward through them from child to parent? The parent is always null.

Here are the entities.

public class Budget
{
    [Key]
    public int Id { get; set; }

    public ICollection<Expense> Expenses { get; set; }

    public ICollection<Income> Incomes { get; set; }
}

public class Expense
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    [Required]
    [StringLength(200)]
    public string ExpenseName { get; set; }

    [Required]
    public decimal Cost { get; set; }

    [StringLength(800)]
    public string Notes { get; set; }

    public DateTime? DueDate { get; set; }

    public Budget Budget { get; set; }
}

public class Income
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    [Required]
    public decimal Amount { get; set; } = 0;

    [Required]
    [StringLength(200)]
    public string Source { get; set; }

    [Required]
    public DateTime? PayDate { get; set; } = DateTime.Now;

    public Budget Budget { get; set; }
}

Here is the repository query.

    public async Task<Budget> GetBudget(int id)
    {
        try
        {
            return await context.Budget
            .Include(e => e.Expenses)
            .Include(i => i.Incomes)
            .SingleAsync(b => b.Id == id);
        }
        catch(Exception x)
        {
            x.ToString();
        }

        return null;
    }

I want to be able to navigate back through the relation from expense to budget to get the Budget.Income collection.

Expected result:

foreach(Expense expense in Budget.Expenses)
{
    if (expense.Budget is not null)
    {
        ICollection<Income> paychecks = expense.Budget.Incomes; // Why is Budget always null?
    }
}

I expected that even if I didn't use the ThenInclude(e => e.Budget) that I should still be able to navigate from the child back to the parent {var budget = expense.Budget}. I'm surprised that this isn't working.

I didn't include the Income entity here, but my goal is to traverse expense.Budget.Incomes to get the collection of incomes in code where I only have access to the expense instance.

After removing ThenInclude(e => e.Budget) I no longer get an error, but the expense.Budget property is still null.

UPDATE

I believe that I found the root cause of my problem. When I added the property Budget to the Expense class I started getting an error when deserializing the objects coming from the API. The HttpClient was throwing an error due to a cyclical reference.

Because I'm fetching the root Budget entity and it's related Expenses, and the Expense has a reference to the Budget I got the cyclical reference error.

I added this code the the startup Blazor server startup class to fix it. I think this is my problem.

services.AddControllersWithViews().AddJsonOptions(x =>
    x.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles);

If I change to ReferenceHandler.Preserve I get a different error.

'The JSON value could not be converted to System.Collections.Generic.ICollection`1[BlazorApp.Data.Models.Expense]. Path: $.expenses | LineNumber: 0 | BytePositionInLine: 34.'

What I don't know and would like to solve is how to make this work so I can get the Budget -> Expenses and have the Expense.Budget property point to it's parent Budget instance. My real issue is probably more related to json serialization and deserialization.

Aaron
  • 413
  • 1
  • 4
  • 14
  • Something is wrong with the entity model. What is the purpose of `Income.Expenses` collection and how it is mapped? – Ivan Stoev Jul 17 '21 at 16:54
  • Income.Expenses is not causing a problem. The problem is Income.Budget property being null. – Aaron Jul 17 '21 at 17:06
  • 1
    Well, this is what you think. But I (with all my experience with EF Core as you can see from my profile) see a problem with that exact property because is messes up the relationships, and might be the cause of the issue you have. – Ivan Stoev Jul 17 '21 at 17:15
  • 1
    @IvanStoev Yes, I am agree with you. – Serge Jul 17 '21 at 17:16
  • I removed Income.Expenses and have updated my code in the question. – Aaron Jul 17 '21 at 17:28
  • These are all of the entity classes and repository code. It's pretty small right now. I thought this would be simple. – Aaron Jul 17 '21 at 17:49
  • 1
    At least it makes clear that you should always show code that reproduces the issue. I wonder if the very first version of you question did. That version looked OK, but still triggered a long shot answer with errors followed by a long tail of edits (question and answer) leading nowhere, but all suggesting you were still developing your code. I suggest you return to a minimal example that you *have tested* to reproduce the issue. – Gert Arnold Jul 17 '21 at 18:06
  • 1
    Here is the point - what are you asking is *proved* to work in EF Core, so apparently there is something wrong with your real model/configuration. I can bet there is something you haven't shown. The initial error message (which you removed) was crystal clear - *"The expression 'e.Budget' is invalid inside an 'Include' operation, since it does not represent a property access:"* This could happen for instance if you decorate `Budget` property with `[NotMapped]`. Many people do, and then forget to mention that "little" detail. So, do you have something like that? Or fluent configuration code? – Ivan Stoev Jul 17 '21 at 18:23
  • I'm going to update my question again to show what I think is the root cause to my problem. I didn't realize that something I did earlier was the cause. I still have a problem I don't have a way to fix though. Give me a bit and I'll update the question. – Aaron Jul 17 '21 at 18:57

2 Answers2

0

You have a bug in your query. Income doesn't depend on expenses, it depends only on budget. The same is about expenses.

So your code can be translated to this

 var budget= await context.Incomes
            .Include(e => e.Expenses)
            .Include(i => i.Incomes)
            .SingleAsync(b => b.Id == id);

foreach (Expense expense in budget.Expenses)
{
      var paychecks = budget.Incomes; 
}

This foreach doesn't make any sense in this case since it just repeats "var paychecks = budget.Incomes;" many times. It is not even saved anywhere.

The same can be done in one line

var paychecks= await context.Incomes
               .Where(b => b.BudgetId == id);
               .ToListAsync();

or if budget is downloaded already

var paychecks = budget.Incomes;

You can not use .ThenInclude(e => e.Budget) since your are querying context.Budget already and as you can see the Budget class doesn't have any extra Budget property.

And Expenses are list. List don't have any Budget property too, only items of the list have. If you try .Include(e => e.Expenses.Budget) it will give a syntax error. But since Expense or Income class has a Budget property this query will be valid

return await context.Incomes
                    .Include(i => i.Budget)
          ....

I think you need only this query that merges budget, income and expenses together

var budget= await context.Budgets
        .Include(e => e.Expenses)
         .Include(i => i.Incomes)
        .FirstOrDefaultAsync(b => b.Id == id);

Also remove public ICollection<Expense> Expenses { get; set; } from Income or make it [NotMapped] if you need it for some reasons.

If you use EF Core 5+, EF Core automatically created foreign keys as the shadow properties. But I personally prefer to have them explicitly. I recommend to add them to your classes (or you still can keep the current version)

public class Budget
{
    [Key]
    public int Id { get; set; }
     .............
   
    [InverseProperty("Budget")]
    public  virtual ICollection<Expense> Expenses { get; set; }

    [InverseProperty("Budget")]
    public  virtual ICollection<Income> Incomes { get; set; }
}

public class Expense
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    ...........

    public int BudgetId { get; set; }

    [ForeignKey(nameof(BudgetId ))]
    [InverseProperty("Expenses")]
    public virtual Budget Budget { get; set; }
}
public class Income
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

   ............

    public int BudgetId { get; }

    [ForeignKey(nameof(BudgetId ))]
    [InverseProperty("Expenses")]
    public virtual Budget Budget { get; set; }

}
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Serge
  • 40,935
  • 4
  • 18
  • 45
  • @Aaron I offered you the query that has everything and it works. If you want expense.Budget you need this context.Expenses. Include (i=>i.Budget). Where(i=i.BudgetId==id ).Tolist(). And that's it. – Serge Jul 17 '21 at 17:40
  • Serge, thank you. What I want though is to query a budget and get the related expenses and incomes. Then I want to be able to navigate back from the expense to the budget. Is it not possible to query a root entity and include its children, then navigate back from the child to the parent? – Aaron Jul 17 '21 at 17:53
  • Yes, some time reverse queries are very usefull. – Serge Jul 17 '21 at 17:54
  • What I don't understand is why the Expense instance doesn't know about it's parent Budget instance. This is a surprise to me. – Aaron Jul 17 '21 at 18:21
  • @Aaron try context.Expenses. Include (i=>i.Budget). Where(i=i.BudgetId==id ).Tolist() and you will see Expense instance knows about it's parent Budget – Serge Jul 17 '21 at 18:24
  • @GertArnold Thank you for your advise. I really appreciate it. Yes I am agree that I have to ask some questions. But I usually don't have much time to wait for answer, since I am answering a dozen questions every day, so I have to use my best guess. You joined later, so you didn't see the first version of PO classes. This is why I offered to fix them. What you see now is mostly my version already – Serge Jul 17 '21 at 18:31
  • I think I've found the real problem. When I add the property Expense.Budget I accidentally created a cyclical reference that the json parser couldn't handle. I'll post more about what I have in my question as another update. Give me a bit to get it together. I still don't have a way around it, but it's the root cause. – Aaron Jul 17 '21 at 18:54
  • @GertArnold Thank you for your advise again. I reviewed my answer and fixed the errors. I am going to keep this style in the future. Pls let me know if you find that something still can approved in my answer. It will be very usefull for me. – Serge Jul 17 '21 at 19:52
  • 1
    Well, in the end OP said "and now for something completely different". As already suspected, not related to the code shown in the question. – Gert Arnold Jul 17 '21 at 20:00
  • @GertArnold I suspected the same – Serge Jul 17 '21 at 20:03
0

Thank you all for the help. Your input helped me know what wasn't the problem.

This fixed my problem.

WebAPI : JSON ReferenceHandler.Preserve

My original question doesn't relate to the actual answer. I did not realize what was really going on. The real problem was a serialization problem. I'm using a hosted Blazor app and have Client and Server projects.

The problem has a number of layers to it. I added the Budget property to the Expense class so that I could traverse back to the budget. After adding the Budget property I started getting an error about cyclical references. This error appears in the Client project during the HttpClient call to the Server controller. I fixed this error by adding the following code to the Server Startup class.

services.AddControllersWithViews().AddJsonOptions(options => {
    options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
    options.JsonSerializerOptions.PropertyNamingPolicy = null; // prevent camel case
});

Days later when I start doing more work I tried to reference the expense.Budget property. Finding that it was always null and started trying to fix it in the entities which is the wrong place.

It turns out that I also needed to pass the JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve to the HttpClient code during the call to the controller.

public async Task<Budget> GetBudget(int id)
{
    Budget budget = null;
    try
    {
        budget = await httpClient.GetFromJsonAsync<Budget>("api/Budget/" + id, new JsonSerializerOptions() { ReferenceHandler = ReferenceHandler.Preserve });
    }
    catch(Exception x)
    {
        x.ToString();
    }
    return budget;
}

This foreach statement now works as expected.

foreach(Expense expense in Budget.Expenses)
{
    if (expense.Budget is not null)
    {
        ICollection<Income> paychecks = expense.Budget.Incomes; // How do I populate budget?
    }
}
Aaron
  • 413
  • 1
  • 4
  • 14