Nick Muller
Nick Muller

Reputation: 2303

Entity Framework Core: How do I ensure a navigation property is loaded?

Say I have the following models:

public class Subject
{
    private List<SubjectResponse> responses;

    public int Id { get; private set; }

    public IEnumerable<SubjectResponse> Responses => responses.ToList();

    public void Foo()
    {
        // How do I check here if Responses fully has been loaded?

        foreach (var response in Responses)
        {
            // ...
        }
    }
}

public class SubjectResponse
{
    public int Id { get; private set; }
}

How do I check if all responses have been loaded in Foo()? I'd probably check for if (Responses is null), but that won't work in all cases.

Here is a minimum example of what could do wrong. In a real app the responses could be loaded at a completely different place. But h This shows how the responses could be fixed up by EF, so it could contain entries, but not all entries.

public async Task Bar()
{
    var response = await dbContext.SubjectResponses.SingleAsync(s => s.Id == 1);
    var subject = await dbContext.Subjects.SingleAsync(s => s.Id == 1);
    subject.Foo();
    // subject.Responses now has a count if 1, when there might actually be more responses.
}

I don't want to use lazy loading, because of performance implications (and because Lazy Loading won't load related entities async). Eager and Explicit loading are fine.

Edit: what I’m mainly looking for is a way to check if the navigation property has been loaded fully, so that I can load it it has not been.

Upvotes: 1

Views: 2571

Answers (3)

Travis Peterson
Travis Peterson

Reputation: 420

If you use the LazyLoader provided in EF Core it will load the navigation properties if they haven't already been loaded. The key thing to remember is if you do this inside a loop it could hit the database many times, one of the core reasons why EF Core went away from this strategy by default.

public class Subject
{
    private ILazyLoader _lazyLoader { get; set; }
    public Subject(ILazyLoader lazyLoader) {
        _lazyLoader = lazyLoader;
    }

    public int Id { get; private set; }

    private IEnumerable<SubjectResponse> _responses;
    public IEnumerable<SubjectResponse> Responses { 
        get => _lazyLoader.Load(this, ref _responses);
        private set => _responses; 
    }

    public void Foo()
    {
        // Accessing the property will trigger a query to get responses if not loaded already
        foreach (var response in Responses)
        {
            // ...
        }
    }
}

Upvotes: 0

Kolazomai
Kolazomai

Reputation: 1171

There is the possibility to throw an exception if a related entity (or a collection of entities) has not been loaded (included), see Non-nullable properties and initialization.

Here is an example of a one-to-many relationship:

    class MyEntity
    {
        // Assume read-only access.
        public IReadOnlyList<MyRelatedEntity> MyRelatedEntities => 
            _myRelatedEntities?.ToList().AsReadOnly() ??
            throw new InvalidOperationException("MyRelatedEntities not loaded.");
        private readonly IEnumerable<MyRelatedEntity>? _myRelatedEntities = null;
    }

    class MyRelatedEntity
    {
        public MyEntity MyEntity
        {
            get => _myEntity ?? 
                throw new InvalidOperationException("MyEntity not loaded.");
            set => _myEntity = value;
        }
        private MyEntity? _myEntity = null;
    }

Entity Framework Core will automatically set the backing field and you can detect if the related entity was loaded.

Unfortunately this approach does not work for optional relationships, or at least I haven't figured out (yet) how to do it.

Upvotes: 1

CodeCaster
CodeCaster

Reputation: 151710

You cannot detect whether all related entities happen to have passed by Entity Framework.

What you show works because the entity from dbContext.SubjectResponses.SingleAsync(s => s.Id == 1) has a SubjectId of 1, and will be cached, and successively be attached to the result of dbContext.Subjects.SingleAsync(s => s.Id == 1).

There is no way for EF, nor for your code, to know that all SubjectResponses with a SubjectId of 1 have been loaded from the database, so you'll have to explicitly load them:

var subject = await dbContext.Subjects.SingleAsync(s => s.Id == 1);
await dbContext.Entity(subject)
               .Reference(s => s.responses)
               .LoadAsync();

But you can't do that, as Subject.responses is private, so you'll have to do that from within your entity's Foo() method, and you'll have to inject your DbContext into your entity, and that'll just become a giant mess.

Why not just do it pragmatically, make Responses a public auto-property and Include() the related entities on beforehand:

var subject = await dbContext.Subjects.Include(s => s.Responses).SingleAsync(s => s.Id == 1);

Upvotes: 1

Related Questions