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

loresoft / EntityChange / 13417967502

19 Feb 2025 04:51PM UTC coverage: 45.43% (+5.7%) from 39.697%
13417967502

push

github

pwelter34
fix failing test

287 of 726 branches covered (39.53%)

Branch coverage included in aggregate %.

563 of 1145 relevant lines covered (49.17%)

73.33 hits per line

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

81.33
/src/EntityChange/EntityComparer.cs
1
using System.Collections;
2
using System.Reflection;
3

4
using EntityChange.Extensions;
5
using EntityChange.Reflection;
6

7
namespace EntityChange;
8

9
/// <summary>
10
/// A class to compare two entities generating a change list.
11
/// </summary>
12
public class EntityComparer : IEntityComparer
13
{
14
    private readonly PathStack _pathStack;
15
    private readonly Stack<IMemberOptions> _memberStack;
16
    private readonly List<ChangeRecord> _changes;
17

18
    /// <summary>
19
    /// Initializes a new instance of the <see cref="EntityComparer"/> class.
20
    /// </summary>
21
    public EntityComparer() : this(EntityConfiguration.Default)
3✔
22
    {
23
    }
3✔
24

25
    /// <summary>
26
    /// Initializes a new instance of the <see cref="EntityComparer"/> class.
27
    /// </summary>
28
    /// <param name="configuration">The configuration.</param>
29
    public EntityComparer(IEntityConfiguration configuration)
26✔
30
    {
31
        Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
26!
32

33
        _changes = [];
26✔
34
        _pathStack = new();
26✔
35
        _memberStack = [];
26✔
36
    }
26✔
37

38

39
    /// <summary>
40
    /// Gets the generator configuration.
41
    /// </summary>
42
    /// <value>
43
    /// The generator configuration.
44
    /// </value>
45
    public IEntityConfiguration Configuration { get; }
37✔
46

47

48
    /// <summary>
49
    /// Compares the specified <paramref name="original"/> and <paramref name="current"/> entities.
50
    /// </summary>
51
    /// <typeparam name="TEntity">The type of the entity.</typeparam>
52
    /// <param name="original">The original entity.</param>
53
    /// <param name="current">The current entity.</param>
54
    /// <returns>A list of changes.</returns>
55
    public IReadOnlyList<ChangeRecord> Compare<TEntity>(TEntity? original, TEntity? current)
56
    {
57
        _changes.Clear();
26✔
58
        _pathStack.Clear();
26✔
59
        _memberStack.Clear();
26✔
60

61
        var type = typeof(TEntity);
26✔
62

63
        CompareType(type, original, current);
26✔
64

65
        return _changes;
26✔
66
    }
67

68

69
    private void CompareType(Type type, object? original, object? current, IMemberOptions? options = null)
70
    {
71
        // both null, nothing to compare
72
        if (original is null && current is null)
346✔
73
            return;
133✔
74

75
        if (type.IsArray)
213✔
76
            CompareArray(original, current, options);
2✔
77
        else if (original is IDictionary || current is IDictionary)
211✔
78
            CompareDictionary(original, current);
8✔
79
        else if (type.IsDictionary(out var keyType, out var elementType))
203!
80
            CompareGenericDictionary(original, current, keyType, elementType);
×
81
        else if (original is IList || current is IList)
203✔
82
            CompareList(original, current, options);
12✔
83
        else if (type.IsCollection())
191✔
84
            CompareCollection(original, current, options);
3✔
85
        else if (type.GetTypeInfo().IsValueType || type == typeof(string))
188✔
86
            CompareValue(original, current, options);
149✔
87
        else
88
            CompareObject(type, original, current);
39✔
89
    }
39✔
90

91
    private void CompareObject(Type type, object? original, object? current)
92
    {
93
        // both null, nothing to compare
94
        if (original is null && current is null)
39!
95
            return;
×
96

97
        if (original is null)
39✔
98
        {
99
            CreateChange(ChangeOperation.Replace, null, current);
1✔
100
            return;
1✔
101
        }
102

103
        if (current is null)
38✔
104
        {
105
            CreateChange(ChangeOperation.Replace, original, null);
1✔
106
            return;
1✔
107
        }
108

109
        var classMapping = Configuration.GetMapping(type);
37✔
110
        foreach (var memberMapping in classMapping.Members.Where(member => !member.Ignored))
965✔
111
        {
112
            var accessor = memberMapping.MemberAccessor;
297✔
113

114
            var originalValue = accessor.GetValue(original);
297✔
115
            var currentValue = accessor.GetValue(current);
297✔
116

117
            var propertyName = accessor.Name;
297✔
118
            var propertyType = accessor.MemberType.GetUnderlyingType();
297✔
119

120
            _memberStack.Push(memberMapping);
297✔
121
            _pathStack.PushProperty(propertyName);
297✔
122
            CompareType(propertyType, originalValue, currentValue, memberMapping);
297✔
123
            _pathStack.Pop();
297✔
124
            _memberStack.TryPop(out _);
297✔
125
        }
126
    }
37✔
127

128

129
    private void CompareDictionary(object? original, object? current)
130
    {
131
        var originalDictionary = original as IDictionary;
8✔
132
        var currentDictionary = current as IDictionary;
8✔
133

134
        // both null, nothing to compare
135
        if (originalDictionary is null && currentDictionary is null)
8!
136
            return;
×
137

138
        CompareByKey(originalDictionary, currentDictionary, static d => d.Keys, static (d, k) => d[k]);
36✔
139
    }
8✔
140

141
    private void CompareGenericDictionary(object? original, object? current, Type keyType, Type elementType)
142
    {
143
        // both null, nothing to compare
144
        if (original is null && current is null)
×
145
            return;
×
146

147
        // TODO improve this, currently slow due to CreateInstance usage
148
        var t = typeof(DictionaryWrapper<,>).MakeGenericType(keyType, elementType);
×
149
        var o = Activator.CreateInstance(t, original) as IDictionaryWrapper;
×
150
        var c = Activator.CreateInstance(t, current) as IDictionaryWrapper;
×
151

152
        if (o is null && c is null)
×
153
            return;
×
154

155
        CompareByKey(o, c, static d => d.GetKeys(), static (d, k) => d.GetValue(k));
×
156
    }
×
157

158

159
    private void CompareArray(object? original, object? current, IMemberOptions? options)
160
    {
161
        var originalArray = original as Array;
2✔
162
        var currentArray = current as Array;
2✔
163

164
        // both null, nothing to compare
165
        if (originalArray is null && currentArray is null)
2!
166
            return;
×
167

168
        if (options?.CollectionComparison == CollectionComparison.ObjectEquality)
2✔
169
            CompareByEquality(originalArray, currentArray, options);
1✔
170
        else
171
            CompareByIndexer(originalArray, currentArray, static t => t.Length, static (t, i) => t.GetValue(i));
4✔
172
    }
1✔
173

174
    private void CompareList(object? original, object? current, IMemberOptions? options)
175
    {
176
        var originalList = original as IList;
12✔
177
        var currentList = current as IList;
12✔
178

179
        // both null, nothing to compare
180
        if (originalList is null && currentList is null)
12!
181
            return;
×
182

183
        if (options?.CollectionComparison == CollectionComparison.ObjectEquality)
12✔
184
            CompareByEquality(originalList, currentList, options);
1✔
185
        else
186
            CompareByIndexer(originalList, currentList, static t => t.Count, static (t, i) => t[i]);
50✔
187
    }
11✔
188

189
    private void CompareCollection(object? original, object? current, IMemberOptions? options)
190
    {
191
        var originalEnumerable = original as IEnumerable;
3✔
192
        var currentEnumerable = current as IEnumerable;
3✔
193

194
        // both null, nothing to compare
195
        if (originalEnumerable is null && currentEnumerable is null)
3!
196
            return;
×
197

198
        if (options?.CollectionComparison == CollectionComparison.ObjectEquality)
3!
199
        {
200
            CompareByEquality(originalEnumerable, currentEnumerable, options);
×
201
            return;
×
202
        }
203

204
        // convert to object array
205
        var originalArray = originalEnumerable?.Cast<object>().ToArray();
3!
206
        var currentArray = currentEnumerable?.Cast<object>().ToArray();
3!
207

208
        CompareByIndexer(originalArray, currentArray, static t => t.Length, static (t, i) => t.GetValue(i));
24✔
209
    }
3✔
210

211

212
    private void CompareValue(object? original, object? current, IMemberOptions? options)
213
    {
214
        var compare = options?.Equality ?? Equals;
149✔
215
        bool areEqual = compare(original, current);
149✔
216

217
        if (areEqual)
149✔
218
            return;
104✔
219

220
        CreateChange(ChangeOperation.Replace, original, current);
45✔
221
    }
45✔
222

223

224
    private void CompareByEquality(IEnumerable? original, IEnumerable? current, IMemberOptions? options)
225
    {
226
        var originalList = original?.Cast<object>().ToList() ?? [];
2!
227
        var currentList = current?.Cast<object>().ToList() ?? [];
2!
228

229
        var compare = options?.Equality ?? Equals;
2!
230
        for (int index = 0; index < currentList.Count; index++)
10✔
231
        {
232
            var v = currentList[index];
3✔
233
            var o = originalList.FirstOrDefault(f => compare(f, v));
8✔
234

235
            if (o == null)
3!
236
            {
237
                // added item
238
                CreateChange(ChangeOperation.Add, null, v);
×
239
                continue;
×
240
            }
241

242
            // remove so can't be reused
243
            originalList.Remove(o);
3✔
244

245
            var t = o.GetType();
3✔
246

247
            _pathStack.PushIndex(index);
3✔
248
            CompareType(t, o, v, options);
3✔
249
            _pathStack.Pop();
3✔
250
        }
251

252
        // removed items
253
        foreach (var v in originalList)
6✔
254
            CreateChange(ChangeOperation.Remove, v, null);
1✔
255
    }
2✔
256

257
    private void CompareByIndexer<T>(T? originalList, T? currentList, Func<T, int> countFactory, Func<T, int, object?> valueFactory)
258
    {
259
        if (countFactory == null)
15!
260
            throw new ArgumentNullException(nameof(countFactory));
×
261
        if (valueFactory == null)
15!
262
            throw new ArgumentNullException(nameof(valueFactory));
×
263

264
        var originalCount = originalList != null ? countFactory(originalList) : 0;
15✔
265
        var currentCount = currentList != null ? countFactory(currentList) : 0;
15✔
266

267
        int commonCount = Math.Min(originalCount, currentCount);
15✔
268

269
        // compare common items
270
        if (commonCount > 0)
15✔
271
        {
272
            for (int i = 0; i < commonCount; i++)
48✔
273
            {
274
                var o = originalList != null ? valueFactory(originalList, i) : null;
14!
275
                var v = currentList != null ? valueFactory(currentList, i) : null;
14!
276

277
                // skip nulls
278
                if (o is null && v is null)
14!
279
                    continue;
280

281
                // get dictionary value type
282
                var t = o?.GetType() ?? v?.GetType();
14!
283
                if (t is null)
14✔
284
                    continue;
285

286
                _pathStack.PushIndex(i);
14✔
287
                CompareType(t, o, v);
14✔
288
                _pathStack.Pop();
14✔
289
            }
290
        }
291

292
        // added items
293
        if (commonCount < currentCount)
15✔
294
        {
295
            for (int i = commonCount; i < currentCount; i++)
20✔
296
            {
297
                var v = currentList != null ? valueFactory(currentList, i) : null;
5!
298

299
                _pathStack.PushIndex(i);
5✔
300
                CreateChange(ChangeOperation.Add, null, v);
5✔
301
                _pathStack.Pop();
5✔
302
            }
303
        }
304

305
        // removed items
306
        if (commonCount < originalCount)
15✔
307
        {
308
            for (int i = commonCount; i < originalCount; i++)
16✔
309
            {
310
                var v = originalList != null ? valueFactory(originalList, i) : null;
4!
311

312
                _pathStack.PushIndex(i);
4✔
313
                CreateChange(ChangeOperation.Remove, v, null);
4✔
314
                _pathStack.Pop();
4✔
315
            }
316
        }
317
    }
15✔
318

319
    private void CompareByKey<T>(T? originalDictionary, T? currentDictionary, Func<T, IEnumerable> keysFactory, Func<T, object, object?> valueFactory)
320
    {
321
        if (keysFactory == null)
8!
322
            throw new ArgumentNullException(nameof(keysFactory));
×
323
        if (valueFactory == null)
8!
324
            throw new ArgumentNullException(nameof(valueFactory));
×
325

326
        List<object> originalKeys = originalDictionary != null ? [.. keysFactory(originalDictionary).Cast<object>()] : [];
8✔
327
        List<object> currentKeys = currentDictionary != null ? [.. keysFactory(currentDictionary).Cast<object>()] : [];
8✔
328

329
        // compare common keys
330
        var commonKeys = originalKeys.Intersect(currentKeys).ToList();
8✔
331
        foreach (var key in commonKeys)
28✔
332
        {
333
            // safe to use indexer because keys are common
334
            var o = originalDictionary != null ? valueFactory(originalDictionary, key) : null;
6!
335
            var v = currentDictionary != null ? valueFactory(currentDictionary, key) : null;
6!
336

337
            // skip nulls
338
            if (o is null && v is null)
6!
339
                continue;
340

341
            // get dictionary value type
342
            var t = o?.GetType() ?? v?.GetType();
6!
343
            if (t is null)
6✔
344
                continue;
345

346
            _pathStack.PushKey(key);
6✔
347
            CompareType(t, o, v);
6✔
348
            _pathStack.Pop();
6✔
349
        }
350

351
        // new key changes
352
        var addedKeys = currentKeys.Except(originalKeys).ToList();
8✔
353
        foreach (var key in addedKeys)
20✔
354
        {
355
            var v = currentDictionary != null ? valueFactory(currentDictionary, key) : null;
2!
356

357
            _pathStack.PushKey(key);
2✔
358
            CreateChange(ChangeOperation.Add, null, v);
2✔
359
            _pathStack.Pop();
2✔
360
        }
361

362
        // removed key changes
363
        var removedKeys = originalKeys.Except(currentKeys).ToList();
8✔
364
        foreach (var key in removedKeys)
20✔
365
        {
366
            var v = originalDictionary != null ? valueFactory(originalDictionary, key) : null;
2!
367

368
            _pathStack.PushKey(key);
2✔
369
            CreateChange(ChangeOperation.Remove, v, null);
2✔
370
            _pathStack.Pop();
2✔
371
        }
372
    }
8✔
373

374

375
    private IMemberOptions? CurrentMember()
376
    {
377
        return _memberStack.Count > 0
61✔
378
            ? _memberStack.Peek()
61✔
379
            : null;
61✔
380
    }
381

382
    private void CreateChange(
383
        ChangeOperation operation,
384
        object? original,
385
        object? current)
386
    {
387
        var currentMember = CurrentMember();
61✔
388
        var propertyName = _pathStack.CurrentName();
61✔
389
        var displayName = currentMember?.DisplayName ?? propertyName.ToTitle();
61✔
390
        var currentPath = _pathStack.ToString();
61✔
391
        var originalFormatted = FormatValue(original, currentMember?.Formatter);
61✔
392
        var currentFormatted = FormatValue(current, currentMember?.Formatter);
61✔
393

394
        var changeRecord = new ChangeRecord
61✔
395
        {
61✔
396
            PropertyName = propertyName,
61✔
397
            DisplayName = displayName,
61✔
398
            Path = currentPath,
61✔
399
            Operation = operation,
61✔
400
            OriginalValue = original,
61✔
401
            CurrentValue = current,
61✔
402
            OriginalFormatted = originalFormatted,
61✔
403
            CurrentFormatted = currentFormatted,
61✔
404
        };
61✔
405

406
        _changes.Add(changeRecord);
61✔
407
    }
61✔
408

409
    private static string? FormatValue(object? value, Func<object?, string?>? formatter)
410
    {
411
        if (value is null)
122✔
412
            return null;
18✔
413

414
        if (formatter is not null)
104✔
415
            return formatter(value);
7✔
416

417
        return value?.ToString();
97!
418
    }
419
}
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