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

lucaslorentz / auto-compute / 12972299212

26 Jan 2025 06:44AM UTC coverage: 83.14% (-0.08%) from 83.217%
12972299212

push

github

lucaslorentz
Transform consistency filter expression before executing

3 of 7 new or added lines in 3 files covered. (42.86%)

1 existing line in 1 file now uncovered.

1790 of 2153 relevant lines covered (83.14%)

431.54 hits per line

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

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

16
namespace LLL.AutoCompute.EFCore;
17

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

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

42
        return await changesProvider.GetChangesAsync();
74✔
43
    }
44

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

54
        var changeCalculation = calculationSelector(ChangeCalculations<TValue>.Instance);
78✔
55

56
        var key = (
78✔
57
            ComputedExpression: new ExpressionCacheKey(computedExpression, ExpressionEqualityComparer.Instance),
78✔
58
            filterExpression: new ExpressionCacheKey(filterExpression, ExpressionEqualityComparer.Instance),
78✔
59
            ChangeCalculation: changeCalculation
78✔
60
        );
78✔
61

62
        var concurrentCreationCache = dbContext.GetService<IConcurrentCreationCache>();
78✔
63

64
        var analyzer = dbContext.Model.GetComputedExpressionAnalyzerOrThrow();
78✔
65

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

69
        var unboundChangesProvider = concurrentCreationCache.GetOrCreate(
78✔
70
            key,
78✔
71
            k => analyzer.CreateChangesProvider(
106✔
72
                entityType.GetOrCreateObservedEntityType(),
106✔
73
                computedExpression,
106✔
74
                filterExpression ?? (x => true),
106✔
75
                changeCalculation)
106✔
76
        );
78✔
77

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

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

122✔
90
            var sortedComputeds = dbContext.Model.GetSortedComputedsOrThrow();
244✔
91

122✔
92
            var changesToProcess = new EFCoreChangeset();
244✔
93

122✔
94
            var observedMembers = sortedComputeds.SelectMany(x => x.ObservedMembers).ToHashSet();
1,226✔
95
            foreach (var observedMember in observedMembers)
2,318✔
96
                await observedMember.CollectChangesAsync(dbContext, changesToProcess);
1,098✔
97

122✔
98
            var updates = new EFCoreChangeset();
244✔
99

122✔
100
            var visitedComputeds = new HashSet<ComputedBase>();
244✔
101

122✔
102
            await UpdateComputedsAsync(sortedComputeds.ToHashSet(), changesToProcess);
244✔
103

122✔
104
            return updates.Count;
244✔
105

122✔
106
            async Task UpdateComputedsAsync(
122✔
107
                IReadOnlySet<ComputedBase> targetComputeds,
122✔
108
                EFCoreChangeset changesToProcess)
122✔
109
            {
122✔
110
                foreach (var computed in sortedComputeds)
2,386✔
111
                {
122✔
112
                    if (!targetComputeds.Contains(computed))
1,128✔
113
                        continue;
122✔
114

122✔
115
                    visitedComputeds.Add(computed);
1,108✔
116

122✔
117
                    var input = new EFCoreComputedInput(dbContext, changesToProcess);
1,108✔
118

122✔
119
                    var newChanges = await computed.Update(input);
1,108✔
120

122✔
121
                    if (newChanges.Count == 0)
1,108✔
122
                        continue;
122✔
123

122✔
124
                    // Detect new changes
122✔
125
                    dbContext.ChangeTracker.DetectChanges();
758✔
126

122✔
127
                    // Register changes in updates, tracking for cyclic updates
122✔
128
                    newChanges.MergeInto(updates, true);
758✔
129

122✔
130
                    // Re-update affected computeds that were already updated
122✔
131
                    var computedsToReUpdate = newChanges.GetAffectedComputeds(visitedComputeds);
758✔
132
                    if (computedsToReUpdate.Count != 0)
758✔
133
                        await UpdateComputedsAsync(computedsToReUpdate, newChanges);
126✔
134

122✔
135
                    // Merge new changes into changesToProcess, to make next computeds in the loop aware of the new changes
122✔
136
                    newChanges.MergeInto(changesToProcess, false);
758✔
137
                }
122✔
138
            }
122✔
139
        });
122✔
140
    }
141

142
    [SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "Optimisation to not create unecessary EntityEntry")]
143
    internal static IEnumerable<EntityEntry> EntityEntriesOfType(this DbContext dbContext, ITypeBase entityType)
144
    {
145
        if (dbContext.ChangeTracker.AutoDetectChangesEnabled)
1,614✔
146
            dbContext.ChangeTracker.DetectChanges();
447✔
147

148
        var dependencies = dbContext.GetDependencies();
1,614✔
149
        return dependencies.StateManager
1,614✔
150
            .Entries
1,614✔
151
            .Where(e => e.EntityType == entityType)
7,158✔
152
            .Select(e => new EntityEntry(e));
4,088✔
153
    }
154

155
    private static readonly ConditionalWeakTable<DbContext, ConcurrentQueue<Func<Task>>> _postSaveActionsQueues = [];
1✔
156
    internal static ConcurrentQueue<Func<Task>> GetComputedPostSaveActionQueue(this DbContext context)
157
    {
158
        return _postSaveActionsQueues.GetValue(context, k => []);
251✔
159
    }
160

161
    internal static async Task<T> WithoutAutoDetectChangesAsync<T>(
162
        this DbContext dbContext,
163
        Func<Task<T>> func)
164
    {
165
        var autoDetectChanges = dbContext.ChangeTracker.AutoDetectChangesEnabled;
122✔
166
        try
167
        {
168
            dbContext.ChangeTracker.AutoDetectChangesEnabled = false;
122✔
169
            return await func();
122✔
170
        }
171
        finally
172
        {
173
            dbContext.ChangeTracker.AutoDetectChangesEnabled = autoDetectChanges;
122✔
174
        }
175
    }
176

177
    public static IQueryable<TEntity> CreateConsistencyQuery<TEntity>(
178
        this DbContext dbContext,
179
        IEntityType entityType,
180
        DateTime since)
181
        where TEntity : class
182
    {
183
        IQueryable<TEntity> query = dbContext.Set<TEntity>(entityType.Name);
×
184

185
        var consistencyFilter = entityType.GetConsistencyFilter();
×
186
        if (consistencyFilter is not null)
×
187
        {
NEW
188
            var analyzer = dbContext.Model.GetComputedExpressionAnalyzerOrThrow();
×
189

190
            var preparedConsistencyFilter = Expression.Lambda<Func<TEntity, bool>>(
×
191
                ReplacingExpressionVisitor.Replace(
×
192
                    consistencyFilter.Parameters[1],
×
193
                    Expression.Constant(since),
×
194
                    consistencyFilter.Body
×
195
                ),
×
196
                consistencyFilter.Parameters[0]);
×
197

NEW
198
            preparedConsistencyFilter = (Expression<Func<TEntity, bool>>)analyzer.RunExpressionModifiers(preparedConsistencyFilter);
×
199

UNCOV
200
            query = query.Where(preparedConsistencyFilter);
×
201
        }
202

203
        return query;
×
204
    }
205
}
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