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

lucaslorentz / auto-compute / 20697889433

04 Jan 2026 07:20PM UTC coverage: 80.825% (+0.07%) from 80.751%
20697889433

push

github

lucaslorentz
Fix mismatch of entity state between input changeset and EF Core state

9 of 9 new or added lines in 2 files covered. (100.0%)

10 existing lines in 1 file now uncovered.

1901 of 2352 relevant lines covered (80.82%)

887.68 hits per line

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

88.75
/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
                    try
244✔
119
                    {
244✔
120
                        visitedComputedMembers.Add(computed);
2,194✔
121

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

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

244✔
128
                        if (newChanges.Count == 0)
2,194✔
129
                            continue;
920✔
130

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

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

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

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

244✔
145
                    }
244✔
146
                    catch (Exception ex)
244✔
147
                    {
244✔
148
                        throw new Exception($"Failed to update computed {computed.ToDebugString()}", ex);
244✔
149
                    }
244✔
150
                }
244✔
151
            }
244✔
152
        });
244✔
153
    }
154

155
    public static async Task<IComputedObserversNotifier> CreateObserversNotifier(this DbContext dbContext)
156
    {
157
        return await WithoutAutoDetectChangesAsync(dbContext, async () =>
244✔
158
        {
244✔
159
            dbContext.ChangeTracker.DetectChanges();
488✔
160

244✔
161
            var allComputedObservers = dbContext.Model.GetAllComputedObservers();
488✔
162

244✔
163
            var changesToProcess = new EFCoreChangeset();
488✔
164

244✔
165
            var observedMembers = allComputedObservers.SelectMany(x => x.ObservedMembers).ToHashSet();
512✔
166
            foreach (var observedMember in observedMembers)
828✔
167
                await observedMember.CollectChangesAsync(dbContext, changesToProcess);
292✔
168

244✔
169
            var observedEntityTypes = allComputedObservers.SelectMany(x => x.ObservedEntityTypes).ToHashSet();
512✔
170
            foreach (var observedEntityType in observedEntityTypes)
812✔
171
                await observedEntityType.CollectChangesAsync(dbContext, changesToProcess);
284✔
172

244✔
173
            var input = new ComputedInput()
488✔
174
                .Set(dbContext)
488✔
175
                .Set(changesToProcess);
488✔
176

244✔
177
            var computedObserversNotifier = new ComputedObserversNotifier();
488✔
178

244✔
179
            foreach (var computed in allComputedObservers)
780✔
180
            {
244✔
181
                var notifier = await computed.CreateObserverNotifier(input);
268✔
182
                if (notifier is not null)
268✔
183
                    computedObserversNotifier.AddNotification(notifier);
268✔
184
            }
244✔
185

244✔
186
            return computedObserversNotifier;
488✔
187
        });
244✔
188
    }
189

190
    [SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "Optimisation to not create unecessary EntityEntry")]
191
    internal static IEnumerable<EntityEntry> EntityEntriesOfType(this DbContext dbContext, ITypeBase entityType)
192
    {
193
        if (dbContext.ChangeTracker.AutoDetectChangesEnabled)
4,075✔
194
            dbContext.ChangeTracker.DetectChanges();
1,145✔
195

196
        var dependencies = dbContext.GetDependencies();
4,075✔
197
        return dependencies.StateManager
4,075✔
198
            .Entries
4,075✔
199
            .Where(e => entityType.IsAssignableFrom(e.EntityType))
17,913✔
200
            .Select(e => new EntityEntry(e));
10,136✔
201
    }
202

203
    internal static async Task<T> WithoutAutoDetectChangesAsync<T>(
204
        this DbContext dbContext,
205
        Func<Task<T>> func)
206
    {
207
        var autoDetectChanges = dbContext.ChangeTracker.AutoDetectChangesEnabled;
488✔
208
        try
209
        {
210
            dbContext.ChangeTracker.AutoDetectChangesEnabled = false;
488✔
211
            return await func();
488✔
212
        }
213
        finally
214
        {
215
            dbContext.ChangeTracker.AutoDetectChangesEnabled = autoDetectChanges;
488✔
216
        }
217
    }
218

219
    public static IQueryable<TEntity> CreateConsistencyQuery<TEntity>(
220
        this DbContext dbContext,
221
        IEntityType entityType,
222
        DateTime since)
223
        where TEntity : class
224
    {
225
        IQueryable<TEntity> query = dbContext.Set<TEntity>(entityType.Name);
×
226

227
        var consistencyFilter = entityType.GetConsistencyFilter();
×
228
        if (consistencyFilter is not null)
×
229
        {
230
            var preparedConsistencyFilter = Expression.Lambda<Func<TEntity, bool>>(
×
UNCOV
231
                ReplacingExpressionVisitor.Replace(
×
232
                    consistencyFilter.Parameters[1],
×
UNCOV
233
                    Expression.Constant(since),
×
UNCOV
234
                    consistencyFilter.Body
×
235
                ),
×
UNCOV
236
                consistencyFilter.Parameters[0]);
×
237

UNCOV
238
            preparedConsistencyFilter = dbContext.PrepareComputedExpression(preparedConsistencyFilter);
×
239

UNCOV
240
            query = query.Where(preparedConsistencyFilter);
×
241
        }
242

243
        return query;
×
244
    }
245

246
    internal static T PrepareComputedExpression<T>(this DbContext dbContext, T expression)
247
        where T : Expression
248
    {
UNCOV
249
        var analyzer = dbContext.Model.GetComputedExpressionAnalyzerOrThrow();
×
250
        expression = (T)analyzer.RunExpressionModifiers(expression);
×
UNCOV
251
        expression = (T)new RemoveChangeComputedTrackingVisitor().Visit(expression);
×
UNCOV
252
        return expression;
×
253
    }
254

255
    internal static T PrepareComputedExpression<T>(this DbContext dbContext, Expression expression)
256
        where T : Expression
257
    {
UNCOV
258
        return (T)dbContext.PrepareComputedExpression(expression);
×
259
    }
260
}
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