简体   繁体   中英

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.

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 Include s are near to where the entities are used.
  • Passing DTOs/ViewModels to the upper layers without passing the entities.

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"); 
}

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)
{
    // ...
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM