/// <summary> /// Loads entities referenced by a collection navigation property from database, unless data is already loaded. /// </summary> /// <param name="entity">Entity instance to load data for.</param> /// <param name="navigationProperty">The navigation property expression.</param> /// <param name="force"><c>false:</c> do nothing if data is already loaded. <c>true:</c> reload data even if already loaded.</param> /// <param name="queryModifier">Modifier for the query that is about to be executed against the database.</param> /// <param name="cancelToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param> public static async Task <CollectionEntry <TEntity, TCollection> > LoadCollectionAsync <TEntity, TCollection>( this HookingDbContext ctx, TEntity entity, Expression <Func <TEntity, IEnumerable <TCollection> > > navigationProperty, bool force = false, Func <IQueryable <TCollection>, IQueryable <TCollection> > queryModifier = null, CancellationToken cancelToken = default) where TEntity : BaseEntity where TCollection : BaseEntity { Guard.NotNull(entity, nameof(entity)); Guard.NotNull(navigationProperty, nameof(navigationProperty)); if (entity.Id == 0) { return(null); } var entry = ctx.Entry(entity); if (entry.State == EfState.Deleted) { return(null); } var collection = entry.Collection(navigationProperty); // TODO: (Core) Entities with hashSets as collections always return true here, as they are never null (for example: Product.ProductVariantAttributes). var isLoaded = collection.CurrentValue != null || collection.IsLoaded; if (!isLoaded && entry.State == EfState.Detached) { try { // Attaching an entity while another instance with same primary key is attached will throw. // First we gonna try to locate an already attached entity. var other = ctx.FindTracked <TEntity>(entity.Id); if (other != null) { // An entity with same key is attached already. So we gonna load the navigation property of attached entity // and copy the result to this detached entity. This way we don't need to attach the source entity. var otherCollection = await ctx.LoadCollectionAsync(other, navigationProperty, force, queryModifier, cancelToken : cancelToken); // Copy collection over to detached entity. collection.CurrentValue = otherCollection.CurrentValue; collection.IsLoaded = true; isLoaded = true; force = false; } else { ctx.Attach(entity); } } catch { // Attach may throw! } } if (force) { collection.IsLoaded = false; isLoaded = false; } if (!isLoaded) { if (queryModifier != null) { var query = queryModifier(collection.Query()); collection.CurrentValue = await query.ToListAsync(cancellationToken : cancelToken); } else { await collection.LoadAsync(cancelToken); } collection.IsLoaded = true; } return(collection); }