• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lucaslorentz / auto-compute / 22113188849

17 Feb 2026 07:24PM UTC coverage: 80.097% (-0.5%) from 80.61%
22113188849

push

github

lucaslorentz
Defer unloaded entities compute in high-cardinality navigations

92 of 126 new or added lines in 13 files covered. (73.02%)

1972 of 2462 relevant lines covered (80.1%)

892.38 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.42
/src/LLL.AutoCompute/EntityContexts/EntityContext.cs
1
using System.Collections.Concurrent;
2
using System.Linq.Expressions;
3
using LLL.AutoCompute.ChangesProviders;
4

5
namespace LLL.AutoCompute.EntityContexts;
6

7
public abstract class EntityContext
8
{
9
    private readonly Expression _expression;
10
    private readonly IReadOnlyList<EntityContext> _parents;
11
    private readonly HashSet<IObservedMember> _observedMembers = [];
680✔
12
    private readonly IList<EntityContext> _childContexts = [];
680✔
13

14
    public EntityContext(Expression expression, IReadOnlyList<EntityContext> parents)
680✔
15
    {
16
        _expression = expression;
680✔
17
        _parents = parents;
680✔
18

19
        foreach (var parent in parents)
1,808✔
20
            parent._childContexts.Add(this);
224✔
21
    }
22

23
    public IReadOnlyList<EntityContext> Parents => _parents;
106✔
24
    public IReadOnlySet<IObservedMember> ObservedMembers => _observedMembers;
80✔
25
    public IEnumerable<EntityContext> ChildContexts => _childContexts;
×
26
    public abstract string ToDebugString();
27

28
    public abstract IObservedEntityType EntityType { get; }
29
    public abstract bool IsTrackingChanges { get; }
30
    public Guid Id { get; } = Guid.NewGuid();
680✔
31
    public Expression Expression => _expression;
×
32

33
    public virtual ChangePropagationTarget PropagationTarget => ChangePropagationTarget.AllEntities;
1,068✔
34

35
    public virtual bool CanResolveLoadedEntities => true;
504✔
36

37
    public void RegisterObservedMember(IObservedMember member)
38
    {
39
        _observedMembers.Add(member);
484✔
40
    }
41

42
    public IEnumerable<IObservedEntityType> GetAllObservedEntityTypes(ChangePropagationTarget? propagationTargetFilter = null)
43
    {
44
        return GetAllWithDuplicates(this, pathPropagationTarget: ChangePropagationTarget.AllEntities).Distinct();
328✔
45

46
        IEnumerable<IObservedEntityType> GetAllWithDuplicates(EntityContext context, ChangePropagationTarget pathPropagationTarget)
47
        {
48
            var strictestPropagationTarget = GetStrictestPropagationTarget(pathPropagationTarget, context.PropagationTarget);
690✔
49

50
            if (propagationTargetFilter is null || strictestPropagationTarget == propagationTargetFilter)
690✔
51
                yield return context.EntityType;
690✔
52

53
            foreach (var childContext in context._childContexts)
2,104✔
54
            {
55
                foreach (var entityType in GetAllWithDuplicates(childContext, strictestPropagationTarget))
1,760✔
56
                    yield return entityType;
518✔
57
            }
58
        }
59
    }
60

61
    public IEnumerable<IObservedMember> GetAllObservedMembers(ChangePropagationTarget? propagationTargetFilter = null)
62
    {
63
        return GetAllWithDuplicates(this, pathPropagationTarget: ChangePropagationTarget.AllEntities).Distinct();
1,946✔
64

65
        IEnumerable<IObservedMember> GetAllWithDuplicates(EntityContext context, ChangePropagationTarget pathPropagationTarget)
66
        {
67
            var strictestPropagationTarget = GetStrictestPropagationTarget(pathPropagationTarget, context.PropagationTarget);
2,400✔
68

69
            if (propagationTargetFilter is null || strictestPropagationTarget == propagationTargetFilter)
2,400✔
70
            {
71
                foreach (var observedMember in context._observedMembers)
7,290✔
72
                    yield return observedMember;
1,872✔
73
            }
74

75
            foreach (var childContext in context._childContexts)
3,128✔
76
            {
77
                foreach (var observedMember in GetAllWithDuplicates(childContext, strictestPropagationTarget))
1,624✔
78
                    yield return observedMember;
394✔
79
            }
80
        }
81

82
    }
83

84
    public async Task<IReadOnlyCollection<object>> GetAffectedEntitiesAsync(ComputedInput input)
85
    {
86
        var entities = new HashSet<object>();
3,174✔
87

88
        input.TryGet<IncrementalContext>(out var incrementalContext);
3,174✔
89

90
        var entityChanges = await EntityType.GetChangesAsync(input);
3,174✔
91
        if (entityChanges is not null)
3,174✔
92
        {
93
            foreach (var entity in entityChanges.Added)
12,044✔
94
                entities.Add(entity);
3,766✔
95
            foreach (var entity in entityChanges.Removed)
4,524✔
96
                entities.Add(entity);
6✔
97
        }
98

99
        foreach (var member in _observedMembers)
13,596✔
100
        {
101
            if (member is IObservedProperty observedProperty)
3,624✔
102
            {
103
                var propertyChanges = await observedProperty.GetChangesAsync(input);
2,378✔
104
                foreach (var propertyChange in propertyChanges)
10,936✔
105
                {
106
                    if (!EntityType.IsInstanceOfType(propertyChange.Entity))
3,090✔
107
                        continue;
108

109
                    entities.Add(propertyChange.Entity);
3,090✔
110
                }
111
            }
112
            else if (member is IObservedNavigation observedNavigation)
1,246✔
113
            {
114
                var navigationChanges = await observedNavigation.GetChangesAsync(input);
1,246✔
115
                foreach (var navigationChange in navigationChanges)
4,596✔
116
                {
117
                    if (!EntityType.IsInstanceOfType(navigationChange.Entity))
1,052✔
118
                        continue;
119

120
                    entities.Add(navigationChange.Entity);
1,052✔
121
                    if (incrementalContext is not null)
1,052✔
122
                    {
123
                        foreach (var addedEntity in navigationChange.Added)
600✔
124
                        {
125
                            incrementalContext.SetShouldLoadAll(addedEntity);
124✔
126
                            incrementalContext.AddCurrentEntity(navigationChange.Entity, observedNavigation, addedEntity);
124✔
127
                        }
128
                        foreach (var removedEntity in navigationChange.Removed)
464✔
129
                        {
130
                            incrementalContext.SetShouldLoadAll(removedEntity);
56✔
131
                            incrementalContext.AddOriginalEntity(navigationChange.Entity, observedNavigation, removedEntity);
56✔
132
                        }
133
                    }
134
                }
135
            }
136
        }
137

138
        foreach (var childContext in _childContexts)
9,036✔
139
        {
140
            var ents = await childContext.GetParentAffectedEntities(input);
1,344✔
141
            foreach (var ent in ents)
4,232✔
142
            {
143
                if (!EntityType.IsInstanceOfType(ent))
772✔
144
                    continue;
145

146
                entities.Add(ent);
772✔
147
            }
148
        }
149

150
        return entities;
3,174✔
151
    }
152

153
    private readonly ConcurrentDictionary<object, EntityContext> _derivedContexts = [];
680✔
154

155
    public EntityContext DeriveWithCache<T>(T key, Func<T, EntityContext> factory)
156
        where T : notnull
157
    {
158
        return _derivedContexts.GetOrAdd(key, k => factory((T)k));
454✔
159
    }
160

161
    public abstract Task<IReadOnlyCollection<object>> GetParentAffectedEntities(ComputedInput input);
162

163
    public async Task<IReadOnlyCollection<object>> ResolveLoadedEntitiesAsync(ComputedInput input)
164
    {
165
        var entities = new HashSet<object>(ReferenceEqualityComparer.Instance);
4✔
166
        var parentLoadedEntities = new HashSet<object>(ReferenceEqualityComparer.Instance);
4✔
167

168
        foreach (var parent in _parents)
8✔
169
        {
NEW
170
            var resolved = await parent.ResolveLoadedEntitiesAsync(input);
×
NEW
171
            foreach (var entity in resolved)
×
NEW
172
                parentLoadedEntities.Add(entity);
×
173
        }
174

175
        if (parentLoadedEntities.Count != 0)
4✔
176
        {
NEW
177
            var loadedEntities = await ResolveLoadedEntitiesFromParentAsync(input, parentLoadedEntities);
×
NEW
178
            foreach (var entity in loadedEntities)
×
NEW
179
                entities.Add(entity);
×
180
        }
181

182
        if (input.TryGet<LoadedEntitySet>(out var loadedEntitySet))
4✔
183
        {
184
            foreach (var entity in loadedEntitySet.Entities.Where(EntityType.IsInstanceOfType))
20✔
185
                entities.Add(entity);
6✔
186
        }
187

188
        return entities;
4✔
189
    }
190

191
    protected virtual async Task<IReadOnlyCollection<object>> ResolveLoadedEntitiesFromParentAsync(
192
        ComputedInput input,
193
        IReadOnlyCollection<object> parentLoadedEntities)
194
    {
NEW
195
        return parentLoadedEntities
×
NEW
196
            .Where(EntityType.IsInstanceOfType)
×
NEW
197
            .ToArray();
×
198
    }
199

200
    public virtual async Task EnrichIncrementalContextAsync(ComputedInput input, IReadOnlyCollection<object> entities)
201
    {
202
        foreach (var childContext in _childContexts)
1,504✔
203
            await childContext.EnrichIncrementalContextFromParentAsync(input, entities);
282✔
204
    }
205

206
    public virtual async Task EnrichIncrementalContextFromParentAsync(ComputedInput input, IReadOnlyCollection<object> parentEntities)
207
    {
208
        await EnrichIncrementalContextAsync(input, parentEntities);
52✔
209
    }
210

211
    public virtual async Task EnrichIncrementalContextTowardsRootAsync(ComputedInput input, IReadOnlyCollection<object> entities)
212
    {
213
        foreach (var parent in Parents)
336✔
214
            await parent.EnrichIncrementalContextTowardsRootAsync(input, entities);
72✔
215
    }
216

217
    public virtual async Task PreLoadNavigationsAsync(ComputedInput input, IReadOnlyCollection<object> entities)
218
    {
219
        foreach (var childContext in _childContexts)
12,512✔
220
            await childContext.PreLoadNavigationsFromParentAsync(input, entities);
1,074✔
221
    }
222

223
    public virtual async Task PreLoadNavigationsFromParentAsync(ComputedInput input, IReadOnlyCollection<object> parentEntities)
224
    {
225
        await PreLoadNavigationsAsync(input, parentEntities);
40✔
226
    }
227

228
    public virtual void MarkNavigationToLoadAll()
229
    {
230
        foreach (var parent in Parents)
28✔
231
            parent.MarkNavigationToLoadAll();
4✔
232
    }
233

234
    protected void ValidateAll(EntityContext? firstNotSupportingResolveLoadedEntities)
235
    {
236
        ValidateSelf();
686✔
237

238
        if (firstNotSupportingResolveLoadedEntities is not null
686✔
239
            && PropagationTarget == ChangePropagationTarget.LoadedEntities)
686✔
240
        {
241
            throw new InvalidOperationException(
6✔
242
                $"EntityContext '{firstNotSupportingResolveLoadedEntities.ToDebugString()}' does not support resolving loaded entities from root and context '{ToDebugString()}' requires loaded entities from root. Remove LoadedEntities propagation target from one of these navigations.");
6✔
243
        }
244

245
        var nextNotSupportingResolveLoadedEntities =
680✔
246
            firstNotSupportingResolveLoadedEntities
680✔
247
            ?? (CanResolveLoadedEntities ? null : this);
680✔
248

249
        foreach (var childContext in _childContexts)
1,794✔
250
            childContext.ValidateAll(nextNotSupportingResolveLoadedEntities);
224✔
251
    }
252

253
    protected virtual void ValidateSelf()
254
    {
255
    }
256

257
    private static ChangePropagationTarget GetStrictestPropagationTarget(params ChangePropagationTarget[] targets)
258
    {
259
        return targets.Any(m => m == ChangePropagationTarget.LoadedEntities)
9,270✔
260
            ? ChangePropagationTarget.LoadedEntities
3,090✔
261
            : ChangePropagationTarget.AllEntities;
3,090✔
262
    }
263
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc