Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoid NullReferenceException when accessing EF Navigation Properties

Many of the bugs I've been fixing lately are a result of null references when accessing navigation properties of objects loaded using entity framework. I believe there must be a flaw in how I'm designing my methods. Here's an example...

A Task contains Many Roles, each Role references a User.

public class Role
{
    public int Id;
    public int User_Id;
    public string Type;
}

public class User
{
    public int Id
    public string Name;
}    

public class Task
{
    public int Id;
    public string Name;
    public string Status;
    public List<Role> Roles;
}

Considering that I would have queried my context like this by mistake and not loaded User...

var task = context.Tasks.Include(x=>x.Roles).FirstOrDefault;

And then I call this method...

public void PrintTask(Task task)
{
    Console.WriteLine(task.Name);
    Console.WriteLine(task.Status);

    foreach(var r in task.Roles)
    {
        Console.WriteLine(r.User.Name); //This will throw NRE because User wasn't loaded
    }
}

I may have built this method with every intention to load Roles and User but next time I use it I may forget that I need both. Ideally the method definition should tell me what data is necessary, but even if I pass in both Task and Roles, I'm still missing Roles->User.

What's the proper way to reference these relationships and be sure that they're loaded in something like this print method? I'm interested in a better design, so "Use Lazy Loading" isn't the answer I'm looking for.

Thanks!

EDIT:

I know I can load the task like this...

var task = context.Tasks.Include(x=>x.Roles.Select(z=>z.User)).FirstOrDefault();

What I want to know is how do I design my method so that when I come back and use it 6 months from now I know what data needs to be loaded in my entities? The method definition doesn't indicate what is necessary to use it. Or how to I block against these NullReferences. There has to be a better design.

like image 421
BZink Avatar asked Dec 20 '11 01:12

BZink


3 Answers

You can use the Select extension method to eager load Users.

var task = context.Tasks.Include(x => x.Roles)
             .Include(x => x.Roles.Select(r => r.User))
             .FirstOrDefault();

Edit:

There are few ways that I can think of to avoid the NRE

  • Integration test using a SQL Server CE/Express database. Unit testing with fake contexts will not work correctly.
  • Loading the entities close to where they are consumed. So that the Includes are near to where the entities are used.
  • Passing DTOs/ViewModels to the upper layers without passing the entities.
like image 88
Eranga Avatar answered Nov 15 '22 06:11

Eranga


Very good question. Here are some possible solutions that, while they don't enforce the avoidance of NREs, they'll provide clues to the caller that they need to Include things:

The first option is to not have your method access a non-guaranteed property of an entity; rather, force the caller to pass both entities:

public void PrintTask(Task task, User taskUser)
{
    // ...
}

Another option is to name the parameter of your method such that it will clue the caller as to what is required:

public void PrintTask(Task taskWithUser)
{
    // ...
}
like image 1
Jacob Avatar answered Nov 15 '22 08:11

Jacob


User should be lazily loaded in your loop—just note though that this is a classic select N + 1 problem that you should fix with another Include.

I think the root problem is either that this particular Role doesn't have a User, or that this particular Role's User has null set for its Name. You'll need to check both for null in your loop

foreach(var r in task.Roles)
{
    if (r.User != null)
        Console.WriteLine(r.User.Name ?? "Name is null"); 
}
like image 1
Adam Rackis Avatar answered Nov 15 '22 06:11

Adam Rackis