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

lucaslorentz / auto-compute / 11986839382

23 Nov 2024 11:38AM UTC coverage: 80.885% (-0.05%) from 80.939%
11986839382

push

github

lucaslorentz
Add support for self referencing computeds

53 of 61 new or added lines in 9 files covered. (86.89%)

38 existing lines in 3 files now uncovered.

1481 of 1831 relevant lines covered (80.88%)

117.05 hits per line

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

82.42
/src/LLL.AutoCompute.EFCore/Internal/ModelExtensions.cs
1
using System.Collections;
2
using System.Reflection;
3
using LLL.AutoCompute.Internal;
4
using Microsoft.EntityFrameworkCore;
5
using Microsoft.EntityFrameworkCore.ChangeTracking;
6
using Microsoft.EntityFrameworkCore.Infrastructure;
7
using Microsoft.EntityFrameworkCore.Metadata;
8

9
namespace LLL.AutoCompute.EFCore.Internal;
10

11
public static class ModelExtensions
12
{
13
    public static object? GetOriginalValue(this NavigationEntry navigationEntry)
14
    {
15
        var entityEntry = navigationEntry.EntityEntry;
197✔
16

17
        if (!navigationEntry.IsLoaded && entityEntry.State != EntityState.Detached)
197✔
18
            navigationEntry.Load();
4✔
19

20
        var dbContext = navigationEntry.EntityEntry.Context;
197✔
21

22
        var baseNavigation = navigationEntry.Metadata;
197✔
23

24
        var input = dbContext.GetComputedInput();
197✔
25

26
        if (baseNavigation.IsCollection)
197✔
27
        {
28
            var collectionAccessor = baseNavigation.GetCollectionAccessor()!;
71✔
29
            var originalValue = collectionAccessor.Create();
71✔
30

31
            if (baseNavigation is ISkipNavigation skipNavigation)
71✔
32
            {
33
                // Add current items that are not new
34
                foreach (var item in navigationEntry.GetEntities())
78✔
35
                {
36
                    var itemEntry = dbContext.Entry(item);
15✔
37

38
                    if (skipNavigation.IsRelationshipNew(input, entityEntry, itemEntry))
15✔
39
                        continue;
40

41
                    collectionAccessor.AddStandalone(originalValue, item);
12✔
42
                }
43

44
                // Add items that were in the collection but were removed
45
                var joinReferenceToOther = skipNavigation.Inverse.ForeignKey.DependentToPrincipal;
24✔
46
                foreach (var joinEntry in input.EntityEntriesOfType(skipNavigation.JoinEntityType))
98✔
47
                {
48
                    var selfReferenceEntry = joinEntry.Reference(skipNavigation.ForeignKey.DependentToPrincipal!);
25✔
49
                    var otherReferenceEntry = joinEntry.Reference(joinReferenceToOther!);
25✔
50
                    if ((joinEntry.State == EntityState.Deleted || selfReferenceEntry.IsModified || otherReferenceEntry.IsModified)
25✔
51
                        && joinEntry.State != EntityState.Added
25✔
52
                        && skipNavigation.ForeignKey.IsConnected(entityEntry.OriginalValues, joinEntry.OriginalValues))
25✔
53
                    {
54
                        collectionAccessor.AddStandalone(originalValue, otherReferenceEntry.GetOriginalValue()!);
8✔
55
                    }
56
                }
57
            }
58
            else if (baseNavigation is INavigation navigation)
47✔
59
            {
60
                // Add current items that are not new
61
                foreach (var item in navigationEntry.GetEntities())
204✔
62
                {
63
                    var itemEntry = dbContext.Entry(item);
55✔
64

65
                    if (navigation.IsRelationshipNew(entityEntry, itemEntry))
55✔
66
                        continue;
67

68
                    collectionAccessor.AddStandalone(originalValue, item);
36✔
69
                }
70

71
                // Add items that were in the collection but were removed
72
                foreach (var itemEntry in input.EntityEntriesOfType(baseNavigation.TargetEntityType))
226✔
73
                {
74
                    if (!navigation.IsRelated(entityEntry, itemEntry)
66✔
75
                        && navigation.WasRelated(entityEntry, itemEntry))
66✔
76
                    {
77
                        collectionAccessor.AddStandalone(originalValue, itemEntry.Entity);
14✔
78
                    }
79
                }
80
            }
81

82
            return originalValue;
71✔
83
        }
84
        else if (baseNavigation is INavigation navigation)
126✔
85
        {
86
            var foreignKey = navigation.ForeignKey;
126✔
87
            if (navigation.IsOnDependent)
126✔
88
            {
89
                var oldKeyValues = foreignKey.Properties
112✔
90
                    .Select(p => entityEntry.OriginalValues[p])
224✔
91
                    .ToArray();
112✔
92

93
                return entityEntry.Context.Find(
112✔
94
                    baseNavigation.TargetEntityType.ClrType,
112✔
95
                    oldKeyValues);
112✔
96
            }
97
            else
98
            {
99
                var inverseNavigation = baseNavigation.Inverse
14✔
100
                    ?? throw new Exception($"No inverse to compute original value for navigation '{baseNavigation}'");
14✔
101

102
                if (entityEntry.State != EntityState.Added)
14✔
103
                {
104
                    var entityOriginalValues = entityEntry.OriginalValues;
14✔
105

106
                    // Original value is the current value
107
                    if (navigationEntry is ReferenceEntry referenceEntry
14✔
108
                        && referenceEntry.TargetEntry is not null
14✔
109
                        && referenceEntry.TargetEntry.State != EntityState.Added
14✔
110
                        && foreignKey.IsConnected(entityOriginalValues, referenceEntry.TargetEntry.OriginalValues))
14✔
111
                    {
112
                        return navigationEntry.CurrentValue;
4✔
113
                    }
114

115
                    // Original value was another value
116
                    foreach (var itemEntry in input.EntityEntriesOfType(baseNavigation.TargetEntityType))
34✔
117
                    {
118
                        var inverseReferenceEntry = itemEntry.Reference(inverseNavigation);
10✔
119
                        if (inverseReferenceEntry.IsModified
10✔
120
                            && foreignKey.IsConnected(entityOriginalValues, itemEntry.OriginalValues))
10✔
121
                        {
122
                            return itemEntry.Entity;
6✔
123
                        }
124
                    }
125
                }
126

127
                return null;
4✔
128
            }
129
        }
130
        else
131
        {
UNCOV
132
            throw new NotSupportedException($"Can't get original value of navigation {baseNavigation}");
×
133
        }
134
    }
135

136
    public static IReadOnlyCollection<object> GetOriginalEntities(this NavigationEntry navigationEntry)
137
    {
138
        var originalValue = navigationEntry.GetOriginalValue();
153✔
139
        if (navigationEntry.Metadata.IsCollection)
153✔
140
        {
141
            if (originalValue is IEnumerable values)
51✔
142
                return values.OfType<object>().ToArray();
51✔
143
        }
144
        else if (originalValue is not null)
102✔
145
        {
146
            return [originalValue];
94✔
147
        }
148

149
        return [];
8✔
150
    }
151

152
    public static IReadOnlyCollection<object> GetEntities(this NavigationEntry navigationEntry)
153
    {
154
        var currentValue = navigationEntry.CurrentValue;
423✔
155
        if (navigationEntry.Metadata.IsCollection)
423✔
156
        {
157
            if (currentValue is IEnumerable values)
212✔
158
                return values.OfType<object>().ToArray();
212✔
159
        }
160
        else if (currentValue is not null)
211✔
161
        {
162
            return [currentValue];
171✔
163
        }
164

165
        return [];
40✔
166
    }
167

168
    public static IReadOnlyCollection<object> GetModifiedEntities(this NavigationEntry navigationEntry)
169
    {
170
        var originalEntities = navigationEntry.EntityEntry.State == EntityState.Added
174✔
171
            ? []
174✔
172
            : navigationEntry.GetOriginalEntities().ToArray();
174✔
173

174
        var currentEntities = navigationEntry.EntityEntry.State == EntityState.Deleted
174✔
175
            ? []
174✔
176
            : navigationEntry.GetEntities().ToArray();
174✔
177

178
        return currentEntities.Except(originalEntities)
174✔
179
            .Concat(originalEntities.Except(currentEntities))
174✔
180
            .ToArray();
174✔
181
    }
182

183
    private static readonly MethodInfo _bulkLoadAsyncTMethodInfo = ((Func<DbContext, IEnumerable<object>, INavigationBase, Task>)BulkLoadAsync<object>)
1✔
184
        .Method.GetGenericMethodDefinition();
1✔
185

186
    public static async Task BulkLoadAsync(this DbContext dbContext, IEnumerable<object> entities, INavigationBase navigation)
187
    {
188
        await (Task)_bulkLoadAsyncTMethodInfo.MakeGenericMethod(navigation.DeclaringEntityType.ClrType)
614✔
189
            .Invoke(null, [dbContext, entities.ToArray(navigation.DeclaringEntityType.ClrType), navigation])!;
614✔
190
    }
191

192
    public static async Task BulkLoadAsync<TEntity>(this DbContext dbContext, IEnumerable<TEntity> entities, INavigationBase navigation)
193
        where TEntity : class
194
    {
195
        var entitiesToLoad = entities.Where(e =>
614✔
196
        {
614✔
197
            var entityEntry = dbContext.Entry(e);
958✔
198
            if (entityEntry.State == EntityState.Detached)
958✔
199
                return false;
614✔
200

614✔
201
            var navigationEntry = entityEntry.Navigation(navigation);
958✔
202
            return !navigationEntry.IsLoaded;
958✔
203
        }).ToArray();
614✔
204

205
        if (entitiesToLoad.Length != 0)
614✔
206
        {
207
            await dbContext.Set<TEntity>(navigation.DeclaringEntityType.Name)
39✔
208
                .Where(e => entitiesToLoad.Contains(e))
39✔
209
                .IgnoreAutoIncludes()
39✔
210
                .Include(e => EF.Property<object>(e, navigation.Name))
39✔
211
                .AsSingleQuery()
39✔
212
                .LoadAsync();
39✔
213
        }
214
    }
215

216
    public static bool WasRelated(
217
        this INavigation navigation,
218
        EntityEntry entry,
219
        EntityEntry relatedEntry)
220
    {
221
        if (entry.State == EntityState.Added
139✔
222
            || relatedEntry.State == EntityState.Added)
139✔
223
            return false;
43✔
224

225
        var (principalEntry, dependantEntry) = navigation.IsOnDependent
96✔
226
            ? (relatedEntry, entry)
96✔
227
            : (entry, relatedEntry);
96✔
228

229
        return navigation.ForeignKey.IsConnected(principalEntry.OriginalValues, dependantEntry.OriginalValues);
96✔
230
    }
231

232
    public static bool IsRelated(
233
        this INavigation navigation,
234
        EntityEntry entry,
235
        EntityEntry relatedEntry)
236
    {
237
        if (entry.State == EntityState.Deleted
166✔
238
            || relatedEntry.State == EntityState.Deleted)
166✔
239
            return false;
6✔
240

241
        var (principalEntry, dependantEntry) = navigation.IsOnDependent
160✔
242
            ? (relatedEntry, entry)
160✔
243
            : (entry, relatedEntry);
160✔
244

245
        return navigation.ForeignKey.IsConnected(principalEntry.CurrentValues, dependantEntry.CurrentValues);
160✔
246
    }
247

248
    public static bool WasRelated(
249
        this ISkipNavigation skipNavigation,
250
        IEFCoreComputedInput input,
251
        EntityEntry entry,
252
        EntityEntry relatedEntry)
253
    {
254
        var entityValues = entry.CurrentValues;
27✔
255
        var relatedEntityValues = relatedEntry.CurrentValues;
27✔
256
        var foreignKey = skipNavigation.ForeignKey;
27✔
257
        var relatedForeignKey = skipNavigation.Inverse!.ForeignKey;
27✔
258
        foreach (var joinEntry in input.EntityEntriesOfType(skipNavigation.JoinEntityType))
93✔
259
        {
260
            if (joinEntry.State == EntityState.Added)
31✔
261
                continue;
262

263
            var wasRelated = foreignKey.IsConnected(entityValues, joinEntry.OriginalValues)
25✔
264
                && relatedForeignKey.IsConnected(relatedEntityValues, joinEntry.OriginalValues);
25✔
265

266
            if (wasRelated)
25✔
267
                return true;
23✔
268
        }
269

270
        return false;
4✔
271
    }
272

273
    public static bool IsRelated(
274
        this ISkipNavigation skipNavigation,
275
        IEFCoreComputedInput input,
276
        EntityEntry entry,
277
        EntityEntry relatedEntry)
278
    {
279
        var entityValues = entry.CurrentValues;
15✔
280
        var relatedEntityValues = relatedEntry.CurrentValues;
15✔
281
        var foreignKey = skipNavigation.ForeignKey;
15✔
282
        var relatedForeignKey = skipNavigation.Inverse!.ForeignKey;
15✔
283
        foreach (var joinEntry in input.EntityEntriesOfType(skipNavigation.JoinEntityType))
51✔
284
        {
285
            if (joinEntry.State == EntityState.Deleted)
15✔
286
                continue;
287

288
            var isRelated = foreignKey.IsConnected(entityValues, joinEntry.CurrentValues)
9✔
289
                && relatedForeignKey.IsConnected(relatedEntityValues, joinEntry.CurrentValues);
9✔
290

291
            if (isRelated)
9✔
292
                return true;
9✔
293
        }
294

295
        return false;
6✔
296
    }
297

298
    public static void LoadJoinEntity(
299
        this ISkipNavigation skipNavigation,
300
        IEFCoreComputedInput input,
301
        EntityEntry entry,
302
        EntityEntry relatedEntry)
303
    {
304
        var inverse = skipNavigation.Inverse;
24✔
305

306
        if (entry.Navigation(skipNavigation).IsLoaded || relatedEntry.Navigation(inverse).IsLoaded)
24✔
307
            return;
24✔
308

UNCOV
309
        var foreignKey = skipNavigation.ForeignKey;
×
UNCOV
310
        var relatedForeignKey = skipNavigation.Inverse.ForeignKey;
×
311

UNCOV
312
        var entityValues = entry.CurrentValues;
×
UNCOV
313
        var relatedEntityValues = relatedEntry.CurrentValues;
×
314

UNCOV
315
        foreach (var joinEntry in input.EntityEntriesOfType(skipNavigation.JoinEntityType))
×
316
        {
UNCOV
317
            if (foreignKey.IsConnected(entityValues, joinEntry.OriginalValues)
×
UNCOV
318
                && relatedForeignKey.IsConnected(relatedEntityValues, joinEntry.OriginalValues))
×
UNCOV
319
                return;
×
320
        }
321

UNCOV
322
        if (input.LoadedJoinEntities.Add((entry, skipNavigation, relatedEntry)))
×
323
        {
UNCOV
324
            var foreignKeyPrincipalAndDependantProperties = foreignKey.PrincipalKey.Properties
×
325
                .Zip(foreignKey.Properties, (p, d) => (Principal: p, Dependant: d));
×
326

UNCOV
327
            var relatedForeignKeyPrincipalAndDependantProperties = relatedForeignKey.PrincipalKey.Properties
×
328
                .Zip(relatedForeignKey.Properties, (p, d) => (Principal: p, Dependant: d));
×
329

UNCOV
330
            var query = input.DbContext.QueryEntityType(skipNavigation.JoinEntityType);
×
331

UNCOV
332
            query = foreignKeyPrincipalAndDependantProperties.Aggregate(
×
333
                query,
×
334
                (c, p) => query.Where(e =>
×
335
                    EF.Property<object>(e, p.Dependant.Name).Equals(entry.CurrentValues[p.Principal])
×
UNCOV
336
                )
×
UNCOV
337
            );
×
338

UNCOV
339
            query = relatedForeignKeyPrincipalAndDependantProperties.Aggregate(
×
340
                query,
×
341
                (c, p) => query.Where(e =>
×
UNCOV
342
                    EF.Property<object>(e, p.Dependant.Name).Equals(relatedEntry.CurrentValues[p.Principal])
×
343
                )
×
344
            );
×
345

346
            query.Load();
×
347
        }
348
    }
349

350
    private static bool IsRelationshipNew(
351
        this INavigation navigation,
352
        EntityEntry principalEntry,
353
        EntityEntry dependentEntry
354
    )
355
    {
356
        return !navigation.WasRelated(principalEntry, dependentEntry)
55✔
357
            && navigation.IsRelated(principalEntry, dependentEntry);
55✔
358
    }
359

360
    private static bool IsRelationshipNew(
361
        this ISkipNavigation skipNavigation,
362
        IEFCoreComputedInput input,
363
        EntityEntry entry,
364
        EntityEntry relatedEntry)
365
    {
366
        return !skipNavigation.WasRelated(input, entry, relatedEntry)
15✔
367
            && skipNavigation.IsRelated(input, entry, relatedEntry);
15✔
368
    }
369

370
    private static bool IsConnected(
371
        this IForeignKey foreignKey,
372
        PropertyValues principalValues,
373
        PropertyValues dependentValues)
374
    {
375
        for (var i = 0; i < foreignKey.PrincipalKey.Properties.Count; i++)
1,318✔
376
        {
377
            var principalProperty = foreignKey.PrincipalKey.Properties[i];
350✔
378
            var dependentProperty = foreignKey.Properties[i];
350✔
379

380
            var principalValue = principalValues[principalProperty];
350✔
381
            var dependentValue = dependentValues[dependentProperty];
350✔
382

383
            if (!principalProperty.GetKeyValueComparer().Equals(principalValue, dependentValue))
350✔
384
                return false;
41✔
385
        }
386

387
        return true;
309✔
388
    }
389

390
    private static IQueryable<object> QueryEntityType(this DbContext dbContext, IEntityType entityType)
391
    {
UNCOV
392
        var genericSetMethod = typeof(DbContext).GetMethod("Set", 1, [typeof(string)])
×
UNCOV
393
            ?? throw new Exception("DbContext generic Set method not found");
×
394

UNCOV
395
        return (IQueryable<object>)genericSetMethod.MakeGenericMethod(entityType.ClrType)
×
UNCOV
396
            .Invoke(dbContext, [entityType.Name])!;
×
397
    }
398
}
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