/// <summary> /// Loads an entity referenced by a 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 event if loaded already.</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 <ReferenceEntry <TEntity, TProperty> > LoadReferenceAsync <TEntity, TProperty>( this HookingDbContext ctx, TEntity entity, Expression <Func <TEntity, TProperty> > navigationProperty, bool force = false, Func <IQueryable <TProperty>, IQueryable <TProperty> > queryModifier = null, CancellationToken cancelToken = default) where TEntity : BaseEntity where TProperty : 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 reference = entry.Reference(navigationProperty); var isLoaded = reference.CurrentValue != null || reference.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 reference property of attached entity // and copy the result to this detached entity. This way we don't need to attach the source entity. var otherReference = await ctx.LoadReferenceAsync(other, navigationProperty, force, queryModifier, cancelToken : cancelToken); // Copy reference over to detached entity. reference.CurrentValue = otherReference.CurrentValue; reference.IsLoaded = true; isLoaded = true; force = false; } else { ctx.Attach(entity); } } catch { // Attach may throw! } } if (force) { reference.IsLoaded = false; isLoaded = false; } if (!isLoaded) { if (queryModifier != null) { var query = queryModifier(reference.Query()); reference.CurrentValue = await query.FirstOrDefaultAsync(cancellationToken : cancelToken); } else { await reference.LoadAsync(cancelToken); } reference.IsLoaded = true; } return(reference); }