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

lucaslorentz / auto-compute / 11750234612

08 Nov 2024 10:06PM UTC coverage: 81.44% (-0.9%) from 82.369%
11750234612

push

github

lucaslorentz
Add initial support for computed navigations and observers

151 of 307 new or added lines in 14 files covered. (49.19%)

42 existing lines in 5 files now uncovered.

1448 of 1778 relevant lines covered (81.44%)

116.07 hits per line

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

83.51
/src/LLL.AutoCompute.EFCore/Metadata/Internal/ModelExtensions.cs
1
using System.Collections;
2
using LLL.AutoCompute.EFCore.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.Metadata.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
        var dbContext = navigationEntry.EntityEntry.Context;
197✔
17

18
        var baseNavigation = navigationEntry.Metadata;
197✔
19

20
        var input = dbContext.GetComputedInput();
197✔
21

22
        if (baseNavigation.IsCollection)
197✔
23
        {
24
            var collectionAccessor = baseNavigation.GetCollectionAccessor()!;
71✔
25
            var originalValue = collectionAccessor.Create();
71✔
26

27
            if (baseNavigation is ISkipNavigation skipNavigation)
71✔
28
            {
29
                // Add current items that are not new
30
                foreach (var item in navigationEntry.GetEntities())
76✔
31
                {
32
                    var itemEntry = dbContext.Entry(item);
14✔
33

34
                    if (skipNavigation.IsRelationshipNew(input, entityEntry, itemEntry))
14✔
35
                        continue;
36

37
                    collectionAccessor.AddStandalone(originalValue, item);
11✔
38
                }
39

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

61
                    if (navigation.IsRelationshipNew(entityEntry, itemEntry))
55✔
62
                        continue;
63

64
                    collectionAccessor.AddStandalone(originalValue, item);
36✔
65
                }
66

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

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

88
                if (entityEntry.State != EntityState.Added)
14✔
89
                {
90
                    var entityOriginalValues = entityEntry.OriginalValues;
14✔
91

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

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

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

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

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

145
        return [];
8✔
146
    }
147

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

161
        return [];
40✔
162
    }
163

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

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

174
        return currentEntities.Except(originalEntities)
174✔
175
            .Concat(originalEntities.Except(currentEntities))
174✔
176
            .ToArray();
174✔
177
    }
178

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

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

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

614✔
214
            var navigationEntry = entityEntry.Navigation(navigation);
958✔
215
            return !navigationEntry.IsLoaded;
958✔
216
        }).ToArray();
614✔
217

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

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

238
        var (principalEntry, dependantEntry) = navigation.IsOnDependent
96✔
239
            ? (relatedEntry, entry)
96✔
240
            : (entry, relatedEntry);
96✔
241

242
        return navigation.ForeignKey.IsConnected(principalEntry.OriginalValues, dependantEntry.OriginalValues);
96✔
243
    }
244

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

254
        var (principalEntry, dependantEntry) = navigation.IsOnDependent
160✔
255
            ? (relatedEntry, entry)
160✔
256
            : (entry, relatedEntry);
160✔
257

258
        return navigation.ForeignKey.IsConnected(principalEntry.CurrentValues, dependantEntry.CurrentValues);
160✔
259
    }
260

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

276
            var wasRelated = foreignKey.IsConnected(entityValues, joinEntry.OriginalValues)
24✔
277
                && relatedForeignKey.IsConnected(relatedEntityValues, joinEntry.OriginalValues);
24✔
278

279
            if (wasRelated)
24✔
280
                return true;
22✔
281
        }
282

283
        return false;
4✔
284
    }
285

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

301
            var isRelated = foreignKey.IsConnected(entityValues, joinEntry.CurrentValues)
9✔
302
                && relatedForeignKey.IsConnected(relatedEntityValues, joinEntry.CurrentValues);
9✔
303

304
            if (isRelated)
9✔
305
                return true;
9✔
306
        }
307

308
        return false;
6✔
309
    }
310

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

319
        if (entry.Navigation(skipNavigation).IsLoaded || relatedEntry.Navigation(inverse).IsLoaded)
24✔
320
            return;
24✔
321

UNCOV
322
        var foreignKey = skipNavigation.ForeignKey;
×
UNCOV
323
        var relatedForeignKey = skipNavigation.Inverse.ForeignKey;
×
324

UNCOV
325
        var entityValues = entry.CurrentValues;
×
UNCOV
326
        var relatedEntityValues = relatedEntry.CurrentValues;
×
327

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

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

UNCOV
340
            var relatedForeignKeyPrincipalAndDependantProperties = relatedForeignKey.PrincipalKey.Properties
×
UNCOV
341
                .Zip(relatedForeignKey.Properties, (p, d) => (Principal: p, Dependant: d));
×
342

UNCOV
343
            var query = input.DbContext.QueryEntityType(skipNavigation.JoinEntityType);
×
344

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

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

359
            query.Load();
×
360
        }
361
    }
362

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

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

383
    private static bool IsConnected(
384
        this IForeignKey foreignKey,
385
        PropertyValues principalValues,
386
        PropertyValues dependentValues)
387
    {
388
        for (var i = 0; i < foreignKey.PrincipalKey.Properties.Count; i++)
1,310✔
389
        {
390
            var principalProperty = foreignKey.PrincipalKey.Properties[i];
348✔
391
            var dependentProperty = foreignKey.Properties[i];
348✔
392

393
            var principalValue = principalValues[principalProperty];
348✔
394
            var dependentValue = dependentValues[dependentProperty];
348✔
395

396
            if (!principalProperty.GetKeyValueComparer().Equals(principalValue, dependentValue))
348✔
397
                return false;
41✔
398
        }
399

400
        return true;
307✔
401
    }
402

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

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