dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.77k stars 3.18k forks source link

How to load ALL items in a lazy collection navigation, when referenced from a child entity? #34622

Closed CheloXL closed 1 month ago

CheloXL commented 2 months ago

I have the below code (very simplified), and configured everything via code configuration (IEntityTypeConfiguration<>). Table schema is configured with builder.HasOne(x => x.DatabaseSchema).WithMany(x => x.TableSchemas);.

Now, I load a specific TableSchema by Id, then access its sibling table schemas by doing tableSchema.DatabaseSchema.TableSchemas. That only returns one table schema, the one I just loaded. It seems that the lazy loader doesn't go to the database to retrieve all the related table schemas.

I think I may understand why this is happening, but I need a way for this to work without having to auto-include the TableSchemas navigation property. That access pattern (accessing the sibling table schemas) is not something that happens a lot (that's why it's configured as lazy).

Is there a way to tell the lazyLoader that I really want that collection to be loaded from DB? I could handle then myself the reference to the loaded collection so my code doesn't keep calling the loader if I already loaded it from DB.

class TableSchema 
{
    private readonly Action<object, string>? _lazyLoader;
    private DatabaseSchema? _databaseSchema;

    private TableSchema(Action<object, string> lazyLoader) : this()
    {
        _lazyLoader = lazyLoader;
    }

    public DatabaseSchema DatabaseSchema
    {
        get { return _lazyLoader.Load(this, ref _databaseSchema)!; }
        set { _databaseSchema = value; }
    }
}

class DatabaseSchema {
    private readonly Action<object, string>? _lazyLoader;
    private List<TableSchema>? _tableSchemas;

    private List<TableSchema> TableSchemaCollection
    {
        get { return _lazyLoader.Load(this, ref _tableSchemas, nameof(TableSchema)) ?? (_tableSchemas = []); }
        init { _tableSchemas = value; }
    }

    private DatabaseSchema(Action<object, string> lazyLoader) : this()
    {
        _lazyLoader = lazyLoader;
    }

    public IReadOnlyCollection<TableSchema> TableSchemas
    {
        get { return TableSchemaCollection; }
    }
}
stevendarby commented 1 month ago

@cincuranet is he returning?

AndriySvyryd commented 1 month ago

@stevendarby Generally, it should work as you expect. Could you share a small runnable project that shows the situation where only one schema is loaded?

stevendarby commented 1 month ago

@AndriySvyryd cc @CheloXL

CheloXL commented 1 month ago

EfCoreTest.zip

@stevendarby, @AndriySvyryd Here you have a small reproducible test. As you can see in the console, EF never queries for the TableSchemas collection and logs that it founds one table, when it should have log 2 entries.

AndriySvyryd commented 1 month ago

@CheloXL The problem is in the extension method. You need to remove the if:

    [return: NotNullIfNotNull(nameof(navigationField))]
    public static TRelated? Load<TRelated>(this Action<object, string>? loader, object entity, ref TRelated? navigationField, [CallerMemberName] string navigationName = null!)
        where TRelated : class
    {
        loader?.Invoke(entity, navigationName);
        return navigationField;
    }