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

lucaslorentz / auto-compute / 11755625072

09 Nov 2024 10:48AM UTC coverage: 79.945% (-0.09%) from 80.033%
11755625072

push

github

lucaslorentz
Add initial support for computed navigations and observers

159 of 359 new or added lines in 22 files covered. (44.29%)

41 existing lines in 7 files now uncovered.

1459 of 1825 relevant lines covered (79.95%)

113.62 hits per line

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

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

8
namespace LLL.AutoCompute.EFCore.Internal;
9

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

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

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

21
        var baseNavigation = navigationEntry.Metadata;
197✔
22

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

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

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

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

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

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

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

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

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

81
            return originalValue;
71✔
82
        }
83
        else if (baseNavigation is INavigation navigation)
126✔
84
        {
85
            var foreignKey = navigation.ForeignKey;
126✔
86
            if (foreignKey.PrincipalEntityType == entityEntry.Metadata)
126✔
87
            {
88
                var inverseNavigation = baseNavigation.Inverse
14✔
89
                    ?? throw new Exception($"No inverse to compute original value for navigation '{baseNavigation}'");
14✔
90

91
                if (entityEntry.State != EntityState.Added)
14✔
92
                {
93
                    var entityOriginalValues = entityEntry.OriginalValues;
14✔
94

95
                    // Original value is the current value
96
                    if (navigationEntry is ReferenceEntry referenceEntry
14✔
97
                        && referenceEntry.TargetEntry is not null
14✔
98
                        && referenceEntry.TargetEntry.State != EntityState.Added
14✔
99
                        && foreignKey.IsConnected(entityOriginalValues, referenceEntry.TargetEntry.OriginalValues))
14✔
100
                    {
101
                        return navigationEntry.CurrentValue;
4✔
102
                    }
103

104
                    // Original value was another value
105
                    foreach (var itemEntry in input.EntityEntriesOfType(baseNavigation.TargetEntityType))
34✔
106
                    {
107
                        var inverseReferenceEntry = itemEntry.Reference(inverseNavigation);
10✔
108
                        if (inverseReferenceEntry.IsModified
10✔
109
                            && foreignKey.IsConnected(entityOriginalValues, itemEntry.OriginalValues))
10✔
110
                        {
111
                            return itemEntry.Entity;
6✔
112
                        }
113
                    }
114
                }
115

116
                return null;
4✔
117
            }
118
            else
119
            {
120
                var oldKeyValues = foreignKey.Properties
112✔
121
                    .Select(p => entityEntry.OriginalValues[p])
224✔
122
                    .ToArray();
112✔
123

124
                return entityEntry.Context.Find(
112✔
125
                    baseNavigation.TargetEntityType.ClrType,
112✔
126
                    oldKeyValues);
112✔
127
            }
128
        }
129
        else
130
        {
UNCOV
131
            throw new NotSupportedException($"Can't get original value of navigation {baseNavigation}");
×
132
        }
133
    }
134

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

148
        return [];
8✔
149
    }
150

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

164
        return [];
40✔
165
    }
166

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

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

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

182
    public static IEntityProperty GetEntityProperty(this IProperty property)
183
    {
184
        return property.GetOrAddRuntimeAnnotationValue(
108✔
185
            ComputedAnnotationNames.EntityMember,
108✔
186
            static (property) =>
108✔
187
            {
108✔
188
                var closedType = typeof(EFCoreEntityProperty<>)
121✔
189
                    .MakeGenericType(property!.DeclaringType.ClrType);
121✔
190
                return (IEntityProperty)Activator.CreateInstance(closedType, property)!;
121✔
191
            },
108✔
192
            property);
108✔
193
    }
194

195
    public static IEntityNavigation GetEntityNavigation(this INavigationBase navigation)
196
    {
197
        return navigation.GetOrAddRuntimeAnnotationValue(
550✔
198
            ComputedAnnotationNames.EntityMember,
550✔
199
            static (navigation) =>
550✔
200
            {
550✔
201
                var closedType = typeof(EFCoreEntityNavigation<,>)
575✔
202
                    .MakeGenericType(navigation!.DeclaringType.ClrType, navigation.TargetEntityType.ClrType);
575✔
203
                return (IEntityNavigation)Activator.CreateInstance(closedType, navigation)!;
575✔
204
            },
550✔
205
            navigation);
550✔
206
    }
207

208
    public static async Task BulkLoadAsync<TEntity>(this DbContext dbContext, IEnumerable<TEntity> entities, INavigationBase navigation)
209
        where TEntity : class
210
    {
211
        var entitiesToLoad = entities.Where(e =>
614✔
212
        {
614✔
213
            var entityEntry = dbContext.Entry(e);
958✔
214
            if (entityEntry.State == EntityState.Detached)
958✔
215
                return false;
614✔
216

614✔
217
            var navigationEntry = entityEntry.Navigation(navigation);
958✔
218
            return !navigationEntry.IsLoaded;
958✔
219
        }).ToArray();
614✔
220

221
        if (entitiesToLoad.Any())
614✔
222
        {
223
            await dbContext.Set<TEntity>()
55✔
224
                .Where(e => entitiesToLoad.Contains(e))
55✔
225
                .IgnoreAutoIncludes()
55✔
226
                .Include(e => EF.Property<object>(e, navigation.Name))
55✔
227
                .AsSingleQuery()
55✔
228
                .LoadAsync();
55✔
229
        }
230
    }
231

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

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

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

248
    public static bool IsRelated(
249
        this INavigation navigation,
250
        EntityEntry entry,
251
        EntityEntry relatedEntry)
252
    {
253
        if (entry.State == EntityState.Deleted
166✔
254
            || relatedEntry.State == EntityState.Deleted)
166✔
255
            return false;
6✔
256

257
        var (principalEntry, dependantEntry) = navigation.IsOnDependent
160✔
258
            ? (relatedEntry, entry)
160✔
259
            : (entry, relatedEntry);
160✔
260

261
        return navigation.ForeignKey.IsConnected(principalEntry.CurrentValues, dependantEntry.CurrentValues);
160✔
262
    }
263

264
    public static bool WasRelated(
265
        this ISkipNavigation skipNavigation,
266
        IEFCoreComputedInput input,
267
        EntityEntry entry,
268
        EntityEntry relatedEntry)
269
    {
270
        var entityValues = entry.CurrentValues;
27✔
271
        var relatedEntityValues = relatedEntry.CurrentValues;
27✔
272
        var foreignKey = skipNavigation.ForeignKey;
27✔
273
        var relatedForeignKey = skipNavigation.Inverse!.ForeignKey;
27✔
274
        foreach (var joinEntry in input.EntityEntriesOfType(skipNavigation.JoinEntityType))
93✔
275
        {
276
            if (joinEntry.State == EntityState.Added)
31✔
277
                continue;
278

279
            var wasRelated = foreignKey.IsConnected(entityValues, joinEntry.OriginalValues)
25✔
280
                && relatedForeignKey.IsConnected(relatedEntityValues, joinEntry.OriginalValues);
25✔
281

282
            if (wasRelated)
25✔
283
                return true;
23✔
284
        }
285

286
        return false;
4✔
287
    }
288

289
    public static bool IsRelated(
290
        this ISkipNavigation skipNavigation,
291
        IEFCoreComputedInput input,
292
        EntityEntry entry,
293
        EntityEntry relatedEntry)
294
    {
295
        var entityValues = entry.CurrentValues;
15✔
296
        var relatedEntityValues = relatedEntry.CurrentValues;
15✔
297
        var foreignKey = skipNavigation.ForeignKey;
15✔
298
        var relatedForeignKey = skipNavigation.Inverse!.ForeignKey;
15✔
299
        foreach (var joinEntry in input.EntityEntriesOfType(skipNavigation.JoinEntityType))
51✔
300
        {
301
            if (joinEntry.State == EntityState.Deleted)
15✔
302
                continue;
303

304
            var isRelated = foreignKey.IsConnected(entityValues, joinEntry.CurrentValues)
9✔
305
                && relatedForeignKey.IsConnected(relatedEntityValues, joinEntry.CurrentValues);
9✔
306

307
            if (isRelated)
9✔
308
                return true;
9✔
309
        }
310

311
        return false;
6✔
312
    }
313

314
    public static void LoadJoinEntity(
315
        this ISkipNavigation skipNavigation,
316
        IEFCoreComputedInput input,
317
        EntityEntry entry,
318
        EntityEntry relatedEntry)
319
    {
320
        var inverse = skipNavigation.Inverse;
24✔
321

322
        if (entry.Navigation(skipNavigation).IsLoaded || relatedEntry.Navigation(inverse).IsLoaded)
24✔
323
            return;
24✔
324

325
        var foreignKey = skipNavigation.ForeignKey;
×
326
        var relatedForeignKey = skipNavigation.Inverse.ForeignKey;
×
327

328
        var entityValues = entry.CurrentValues;
×
329
        var relatedEntityValues = relatedEntry.CurrentValues;
×
330

UNCOV
331
        foreach (var joinEntry in input.EntityEntriesOfType(skipNavigation.JoinEntityType))
×
332
        {
UNCOV
333
            if (foreignKey.IsConnected(entityValues, joinEntry.OriginalValues)
×
UNCOV
334
                && relatedForeignKey.IsConnected(relatedEntityValues, joinEntry.OriginalValues))
×
UNCOV
335
                return;
×
336
        }
337

UNCOV
338
        if (input.LoadedJoinEntities.Add((entry, skipNavigation, relatedEntry)))
×
339
        {
UNCOV
340
            var foreignKeyPrincipalAndDependantProperties = foreignKey.PrincipalKey.Properties
×
UNCOV
341
                .Zip(foreignKey.Properties, (p, d) => (Principal: p, Dependant: d));
×
342

UNCOV
343
            var relatedForeignKeyPrincipalAndDependantProperties = relatedForeignKey.PrincipalKey.Properties
×
UNCOV
344
                .Zip(relatedForeignKey.Properties, (p, d) => (Principal: p, Dependant: d));
×
345

UNCOV
346
            var query = input.DbContext.QueryEntityType(skipNavigation.JoinEntityType);
×
347

UNCOV
348
            query = foreignKeyPrincipalAndDependantProperties.Aggregate(
×
UNCOV
349
                query,
×
UNCOV
350
                (c, p) => query.Where(e =>
×
UNCOV
351
                    EF.Property<object>(e, p.Dependant.Name).Equals(entry.CurrentValues[p.Principal])
×
UNCOV
352
                )
×
UNCOV
353
            );
×
354

UNCOV
355
            query = relatedForeignKeyPrincipalAndDependantProperties.Aggregate(
×
UNCOV
356
                query,
×
UNCOV
357
                (c, p) => query.Where(e =>
×
UNCOV
358
                    EF.Property<object>(e, p.Dependant.Name).Equals(relatedEntry.CurrentValues[p.Principal])
×
UNCOV
359
                )
×
UNCOV
360
            );
×
361

UNCOV
362
            query.Load();
×
363
        }
364
    }
365

366
    private static bool IsRelationshipNew(
367
        this INavigation navigation,
368
        EntityEntry principalEntry,
369
        EntityEntry dependentEntry
370
    )
371
    {
372
        return !navigation.WasRelated(principalEntry, dependentEntry)
55✔
373
            && navigation.IsRelated(principalEntry, dependentEntry);
55✔
374
    }
375

376
    private static bool IsRelationshipNew(
377
        this ISkipNavigation skipNavigation,
378
        IEFCoreComputedInput input,
379
        EntityEntry entry,
380
        EntityEntry relatedEntry)
381
    {
382
        return !skipNavigation.WasRelated(input, entry, relatedEntry)
15✔
383
            && skipNavigation.IsRelated(input, entry, relatedEntry);
15✔
384
    }
385

386
    private static bool IsConnected(
387
        this IForeignKey foreignKey,
388
        PropertyValues principalValues,
389
        PropertyValues dependentValues)
390
    {
391
        for (var i = 0; i < foreignKey.PrincipalKey.Properties.Count; i++)
1,318✔
392
        {
393
            var principalProperty = foreignKey.PrincipalKey.Properties[i];
350✔
394
            var dependentProperty = foreignKey.Properties[i];
350✔
395

396
            var principalValue = principalValues[principalProperty];
350✔
397
            var dependentValue = dependentValues[dependentProperty];
350✔
398

399
            if (!principalProperty.GetKeyValueComparer().Equals(principalValue, dependentValue))
350✔
400
                return false;
41✔
401
        }
402

403
        return true;
309✔
404
    }
405

406
    private static IQueryable<object> QueryEntityType(this DbContext dbContext, IEntityType entityType)
407
    {
UNCOV
408
        var genericSetMethod = typeof(DbContext).GetMethod("Set", 1, [typeof(string)])
×
UNCOV
409
            ?? throw new Exception("DbContext generic Set method not found");
×
410

UNCOV
411
        return (IQueryable<object>)genericSetMethod.MakeGenericMethod(entityType.ClrType)
×
UNCOV
412
            .Invoke(dbContext, [entityType.Name])!;
×
413
    }
414
}
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

© 2025 Coveralls, Inc