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
Include
s are near to where the entities are used.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)
{
// ...
}
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");
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With