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

lucaslorentz / auto-compute / 20616058029

31 Dec 2025 09:19AM UTC coverage: 81.446% (-0.2%) from 81.665%
20616058029

push

github

lucaslorentz
Optimise complex entity contexts

21 of 24 new or added lines in 7 files covered. (87.5%)

23 existing lines in 2 files now uncovered.

1892 of 2323 relevant lines covered (81.45%)

874.94 hits per line

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

88.16
/src/LLL.AutoCompute.EFCore/DbContextExtensions.cs
1
using System.Diagnostics.CodeAnalysis;
2
using System.Linq.Expressions;
3
using LLL.AutoCompute.ChangesProviders;
4
using LLL.AutoCompute.EFCore.Caching;
5
using LLL.AutoCompute.EFCore.Internal;
6
using LLL.AutoCompute.EFCore.Metadata.Internal;
7
using LLL.AutoCompute.Internal.ExpressionVisitors;
8
using Microsoft.EntityFrameworkCore;
9
using Microsoft.EntityFrameworkCore.ChangeTracking;
10
using Microsoft.EntityFrameworkCore.Infrastructure;
11
using Microsoft.EntityFrameworkCore.Internal;
12
using Microsoft.EntityFrameworkCore.Metadata;
13
using Microsoft.EntityFrameworkCore.Query;
14

15
namespace LLL.AutoCompute.EFCore;
16

17
public static class DbContextExtensions
18
{
19
    public static DbContextOptionsBuilder UseAutoCompute(
20
        this DbContextOptionsBuilder optionsBuilder,
21
        Action<ComputedOptionsBuilder>? configureOptions = null)
22
    {
23
        var builder = new ComputedOptionsBuilder(optionsBuilder);
186✔
24
        configureOptions?.Invoke(builder);
186✔
25
        ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(builder.Build());
186✔
26
        return optionsBuilder;
186✔
27
    }
28

29
    public static async Task<IReadOnlyDictionary<TEntity, TChange>> GetChangesAsync<TEntity, TValue, TChange>(
30
        this DbContext dbContext,
31
        Expression<Func<TEntity, TValue>> computedExpression,
32
        Expression<Func<TEntity, bool>>? filterExpression,
33
        ChangeCalculatorSelector<TValue, TChange> calculationSelector)
34
        where TEntity : class
35
    {
36
        var changesProvider = dbContext.GetChangesProvider(
148✔
37
            computedExpression,
148✔
38
            filterExpression,
148✔
39
            calculationSelector);
148✔
40

41
        return await changesProvider.GetChangesAsync();
148✔
42
    }
43

44
    public static EFCoreChangesProvider<TEntity, TChange> GetChangesProvider<TEntity, TValue, TChange>(
45
        this DbContext dbContext,
46
        Expression<Func<TEntity, TValue>> computedExpression,
47
        Expression<Func<TEntity, bool>>? filterExpression,
48
        ChangeCalculatorSelector<TValue, TChange> calculationSelector)
49
        where TEntity : class
50
    {
51
        filterExpression ??= static e => true;
156✔
52

53
        var changeCalculator = calculationSelector(ChangeCalculatorFactory<TValue>.Instance);
156✔
54

55
        var key = (
156✔
56
            ComputedExpression: new ExpressionCacheKey(computedExpression, ExpressionEqualityComparer.Instance),
156✔
57
            filterExpression: new ExpressionCacheKey(filterExpression, ExpressionEqualityComparer.Instance),
156✔
58
            ChangeCalculator: changeCalculator
156✔
59
        );
156✔
60

61
        var concurrentCreationCache = dbContext.GetService<IConcurrentCreationCache>();
156✔
62

63
        var analyzer = dbContext.Model.GetComputedExpressionAnalyzerOrThrow();
156✔
64

65
        var entityType = dbContext.Model.FindEntityType(typeof(TEntity))
156✔
66
            ?? throw new Exception($"No EntityType found for {typeof(TEntity)}");
156✔
67

68
        var unboundChangesProvider = concurrentCreationCache.GetOrCreate(
156✔
69
            key,
156✔
70
            k => analyzer.CreateChangesProvider(
212✔
71
                entityType.GetOrCreateObservedEntityType(),
212✔
72
                computedExpression,
212✔
73
                filterExpression ?? (x => true),
212✔
74
                changeCalculator)
212✔
75
        );
156✔
76

77
        return new EFCoreChangesProvider<TEntity, TChange>(
156✔
78
            unboundChangesProvider,
156✔
79
            dbContext
156✔
80
        );
156✔
81
    }
82

83
    public static async Task<int> UpdateComputedsAsync(this DbContext dbContext)
84
    {
85
        return await WithoutAutoDetectChangesAsync(dbContext, async () =>
244✔
86
        {
244✔
87
            dbContext.ChangeTracker.DetectChanges();
488✔
88

244✔
89
            var sortedComputedMembers = dbContext.Model.GetAllComputedMembers();
488✔
90

244✔
91
            var changesToProcess = new EFCoreChangeset();
488✔
92

244✔
93
            var observedMembers = sortedComputedMembers.SelectMany(x => x.ObservedMembers).ToHashSet();
2,428✔
94
            foreach (var observedMember in observedMembers)
4,636✔
95
                await observedMember.CollectChangesAsync(dbContext, changesToProcess);
2,196✔
96

244✔
97
            var observedEntityTypes = sortedComputedMembers.SelectMany(x => x.ObservedEntityTypes).ToHashSet();
2,428✔
98
            foreach (var observedEntityType in observedEntityTypes)
1,732✔
99
                await observedEntityType.CollectChangesAsync(dbContext, changesToProcess);
744✔
100

244✔
101
            var updates = new EFCoreChangeset();
488✔
102

244✔
103
            var visitedComputedMembers = new HashSet<ComputedMember>();
488✔
104

244✔
105
            await UpdateComputedsAsync(sortedComputedMembers.ToHashSet(), changesToProcess);
488✔
106

244✔
107
            return updates.Count;
488✔
108

244✔
109
            async Task UpdateComputedsAsync(
244✔
110
                IReadOnlySet<ComputedMember> targetComputeds,
244✔
111
                EFCoreChangeset changesToProcess)
244✔
112
            {
244✔
113
                foreach (var computed in sortedComputedMembers)
4,752✔
114
                {
244✔
115
                    if (!targetComputeds.Contains(computed))
2,244✔
116
                        continue;
244✔
117

244✔
118
                    visitedComputedMembers.Add(computed);
2,194✔
119

244✔
120
                    var input = new ComputedInput()
2,194✔
121
                        .Set(dbContext)
2,194✔
122
                        .Set(changesToProcess);
2,194✔
123

244✔
124
                    var newChanges = await computed.Update(input);
2,194✔
125

244✔
126
                    if (newChanges.Count == 0)
2,194✔
127
                        continue;
244✔
128

244✔
129
                    // Detect new changes
244✔
130
                    dbContext.ChangeTracker.DetectChanges();
1,518✔
131

244✔
132
                    // Register changes in updates, tracking for cyclic updates
244✔
133
                    newChanges.MergeInto(updates, true);
1,518✔
134

244✔
135
                    // Re-update affected computeds that were already updated
244✔
136
                    var computedsToReUpdate = newChanges.GetAffectedComputedMembers(visitedComputedMembers);
1,518✔
137
                    if (computedsToReUpdate.Count != 0)
1,518✔
138
                        await UpdateComputedsAsync(computedsToReUpdate, newChanges);
254✔
139

244✔
140
                    // Merge new changes into changesToProcess, to make next computeds in the loop aware of the new changes
244✔
141
                    newChanges.MergeInto(changesToProcess, false);
1,518✔
142
                }
244✔
143
            }
244✔
144
        });
244✔
145
    }
146

147
    public static async Task<IComputedObserversNotifier> CreateObserversNotifier(this DbContext dbContext)
148
    {
149
        return await WithoutAutoDetectChangesAsync(dbContext, async () =>
244✔
150
        {
244✔
151
            dbContext.ChangeTracker.DetectChanges();
488✔
152

244✔
153
            var allComputedObservers = dbContext.Model.GetAllComputedObservers();
488✔
154

244✔
155
            var changesToProcess = new EFCoreChangeset();
488✔
156

244✔
157
            var observedMembers = allComputedObservers.SelectMany(x => x.ObservedMembers).ToHashSet();
512✔
158
            foreach (var observedMember in observedMembers)
828✔
159
                await observedMember.CollectChangesAsync(dbContext, changesToProcess);
292✔
160

244✔
161
            var observedEntityTypes = allComputedObservers.SelectMany(x => x.ObservedEntityTypes).ToHashSet();
512✔
162
            foreach (var observedEntityType in observedEntityTypes)
812✔
163
                await observedEntityType.CollectChangesAsync(dbContext, changesToProcess);
284✔
164

244✔
165
            var input = new ComputedInput()
488✔
166
                .Set(dbContext)
488✔
167
                .Set(changesToProcess);
488✔
168

244✔
169
            var computedObserversNotifier = new ComputedObserversNotifier();
488✔
170

244✔
171
            foreach (var computed in allComputedObservers)
780✔
172
            {
244✔
173
                var notifier = await computed.CreateObserverNotifier(input);
268✔
174
                if (notifier is not null)
268✔
175
                    computedObserversNotifier.AddNotification(notifier);
268✔
176
            }
244✔
177

244✔
178
            return computedObserversNotifier;
488✔
179
        });
244✔
180
    }
181

182
    [SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "Optimisation to not create unecessary EntityEntry")]
183
    internal static IEnumerable<EntityEntry> EntityEntriesOfType(this DbContext dbContext, ITypeBase entityType)
184
    {
185
        if (dbContext.ChangeTracker.AutoDetectChangesEnabled)
4,074✔
186
            dbContext.ChangeTracker.DetectChanges();
1,144✔
187

188
        var dependencies = dbContext.GetDependencies();
4,074✔
189
        return dependencies.StateManager
4,074✔
190
            .Entries
4,074✔
191
            .Where(e => entityType.IsAssignableFrom(e.EntityType))
17,904✔
192
            .Select(e => new EntityEntry(e));
10,134✔
193
    }
194

195
    internal static async Task<T> WithoutAutoDetectChangesAsync<T>(
196
        this DbContext dbContext,
197
        Func<Task<T>> func)
198
    {
199
        var autoDetectChanges = dbContext.ChangeTracker.AutoDetectChangesEnabled;
488✔
200
        try
201
        {
202
            dbContext.ChangeTracker.AutoDetectChangesEnabled = false;
488✔
203
            return await func();
488✔
204
        }
205
        finally
206
        {
207
            dbContext.ChangeTracker.AutoDetectChangesEnabled = autoDetectChanges;
488✔
208
        }
209
    }
210

211
    public static IQueryable<TEntity> CreateConsistencyQuery<TEntity>(
212
        this DbContext dbContext,
213
        IEntityType entityType,
214
        DateTime since)
215
        where TEntity : class
216
    {
UNCOV
217
        IQueryable<TEntity> query = dbContext.Set<TEntity>(entityType.Name);
×
218

219
        var consistencyFilter = entityType.GetConsistencyFilter();
×
UNCOV
220
        if (consistencyFilter is not null)
×
221
        {
UNCOV
222
            var preparedConsistencyFilter = Expression.Lambda<Func<TEntity, bool>>(
×
223
                ReplacingExpressionVisitor.Replace(
×
224
                    consistencyFilter.Parameters[1],
×
225
                    Expression.Constant(since),
×
226
                    consistencyFilter.Body
×
227
                ),
×
228
                consistencyFilter.Parameters[0]);
×
229

UNCOV
230
            preparedConsistencyFilter = dbContext.PrepareComputedExpression(preparedConsistencyFilter);
×
231

UNCOV
232
            query = query.Where(preparedConsistencyFilter);
×
233
        }
234

UNCOV
235
        return query;
×
236
    }
237

238
    internal static T PrepareComputedExpression<T>(this DbContext dbContext, T expression)
239
        where T : Expression
240
    {
UNCOV
241
        var analyzer = dbContext.Model.GetComputedExpressionAnalyzerOrThrow();
×
UNCOV
242
        expression = (T)analyzer.RunExpressionModifiers(expression);
×
UNCOV
243
        expression = (T)new RemoveChangeComputedTrackingVisitor().Visit(expression);
×
UNCOV
244
        return expression;
×
245
    }
246

247
    internal static T PrepareComputedExpression<T>(this DbContext dbContext, Expression expression)
248
        where T : Expression
249
    {
UNCOV
250
        return (T)dbContext.PrepareComputedExpression(expression);
×
251
    }
252
}
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