Skip to content

Commit 812d6c8

Browse files
committed
Collection change tracking for primitive collections
Part of #25364
1 parent bd16563 commit 812d6c8

File tree

14 files changed

+1016
-47
lines changed

14 files changed

+1016
-47
lines changed

src/EFCore.Cosmos/Storage/Internal/CosmosTypeMapping.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,10 @@ protected CosmosTypeMapping(CoreTypeMappingParameters parameters)
6262
/// </summary>
6363
public override CoreTypeMapping Clone(
6464
ValueConverter? converter,
65+
ValueComparer? comparer = null,
6566
CoreTypeMapping? elementMapping = null,
6667
JsonValueReaderWriter? jsonValueReaderWriter = null)
67-
=> new CosmosTypeMapping(Parameters.WithComposedConverter(converter, elementMapping, jsonValueReaderWriter));
68+
=> new CosmosTypeMapping(Parameters.WithComposedConverter(converter, comparer, elementMapping, jsonValueReaderWriter));
6869

6970
/// <summary>
7071
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to

src/EFCore.InMemory/Storage/Internal/InMemoryTypeMapping.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,10 @@ private InMemoryTypeMapping(CoreTypeMappingParameters parameters)
5555
/// </summary>
5656
public override CoreTypeMapping Clone(
5757
ValueConverter? converter,
58+
ValueComparer? comparer = null,
5859
CoreTypeMapping? elementMapping = null,
5960
JsonValueReaderWriter? jsonValueReaderWriter = null)
60-
=> new InMemoryTypeMapping(Parameters.WithComposedConverter(converter, elementMapping, jsonValueReaderWriter));
61+
=> new InMemoryTypeMapping(Parameters.WithComposedConverter(converter, comparer, elementMapping, jsonValueReaderWriter));
6162

6263
/// <summary>
6364
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to

src/EFCore.Relational/Storage/RelationalTypeMapping.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -230,15 +230,17 @@ public RelationalTypeMappingParameters WithScale(int? scale)
230230
/// converter composed with any existing converter and set on the new parameter object.
231231
/// </summary>
232232
/// <param name="converter">The converter.</param>
233+
/// <param name="comparer">The comparer.</param>
233234
/// <param name="elementMapping">The element mapping, or <see langword="null" /> for non-collection mappings.</param>
234235
/// <param name="jsonValueReaderWriter">The JSON reader/writer, or <see langword="null" /> to leave unchanged.</param>
235236
/// <returns>The new parameter object.</returns>
236237
public RelationalTypeMappingParameters WithComposedConverter(
237238
ValueConverter? converter,
239+
ValueComparer? comparer,
238240
CoreTypeMapping? elementMapping,
239241
JsonValueReaderWriter? jsonValueReaderWriter)
240242
=> new(
241-
CoreParameters.WithComposedConverter(converter, elementMapping, jsonValueReaderWriter),
243+
CoreParameters.WithComposedConverter(converter, comparer, elementMapping, jsonValueReaderWriter),
242244
StoreType,
243245
StoreTypePostfix,
244246
DbType,
@@ -428,9 +430,10 @@ public virtual RelationalTypeMapping Clone(int? precision, int? scale)
428430
/// <inheritdoc />
429431
public override CoreTypeMapping Clone(
430432
ValueConverter? converter,
433+
ValueComparer? comparer = null,
431434
CoreTypeMapping? elementMapping = null,
432435
JsonValueReaderWriter? jsonValueReaderWriter = null)
433-
=> Clone(Parameters.WithComposedConverter(converter, elementMapping, jsonValueReaderWriter));
436+
=> Clone(Parameters.WithComposedConverter(converter, comparer, elementMapping, jsonValueReaderWriter));
434437

435438
/// <summary>
436439
/// Clones the type mapping to update facets from the mapping info, if needed.

src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs

+11-2
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,10 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo)
224224
Type modelType,
225225
Type? providerType,
226226
CoreTypeMapping? elementMapping)
227-
=> TryFindJsonCollectionMapping(
227+
{
228+
var elementType = modelType.TryGetElementType(typeof(IEnumerable<>))!;
229+
230+
return TryFindJsonCollectionMapping(
228231
info.CoreTypeMappingInfo, modelType, providerType, ref elementMapping, out var collectionReaderWriter)
229232
? (RelationalTypeMapping)FindMapping(
230233
info.WithConverter(
@@ -233,11 +236,17 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo)
233236
.Clone(
234237
(ValueConverter)Activator.CreateInstance(
235238
typeof(CollectionToJsonStringConverter<>).MakeGenericType(
236-
modelType.TryGetElementType(typeof(IEnumerable<>))!),
239+
elementType),
237240
collectionReaderWriter!)!,
241+
(ValueComparer?)Activator.CreateInstance(
242+
elementType.IsNullableValueType()
243+
? typeof(NullableValueTypeListComparer<>).MakeGenericType(elementType.UnwrapNullableType())
244+
: typeof(ListComparer<>).MakeGenericType(elementType),
245+
elementMapping?.Comparer),
238246
elementMapping,
239247
collectionReaderWriter)
240248
: null;
249+
}
241250

242251
/// <summary>
243252
/// Finds the type mapping for a given <see cref="IProperty" />.
+144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.EntityFrameworkCore.ChangeTracking;
5+
6+
/// <summary>
7+
/// A <see cref="ValueComparer{T}"/> for lists of primitive items. The list can be typed as <see cref="IEnumerable{T}"/>,
8+
/// but can only be used with instances that implement <see cref="IList{T}"/>.
9+
/// </summary>
10+
/// <remarks>
11+
/// <para>
12+
/// This comparer should be used for reference types and non-nullable value types. Use
13+
/// <see cref="NullableValueTypeListComparer{TElement}"/> for nullable value types.
14+
/// </para>
15+
/// <para>
16+
/// See <see href="https://aka.ms/efcore-docs-value-comparers">EF Core value comparers</see> for more information and examples.
17+
/// </para>
18+
/// </remarks>
19+
/// <typeparam name="TElement">The element type.</typeparam>
20+
public sealed class ListComparer<TElement> : ValueComparer<IEnumerable<TElement>>
21+
{
22+
/// <summary>
23+
/// Creates a new instance of the list comparer.
24+
/// </summary>
25+
/// <param name="elementComparer">The comparer to use for comparing elements.</param>
26+
public ListComparer(ValueComparer<TElement> elementComparer)
27+
: base(
28+
(a, b) => Compare(a, b, elementComparer),
29+
o => GetHashCode(o, elementComparer),
30+
source => Snapshot(source, elementComparer))
31+
{
32+
}
33+
34+
private static bool Compare(IEnumerable<TElement>? a, IEnumerable<TElement>? b, ValueComparer<TElement> elementComparer)
35+
{
36+
if (ReferenceEquals(a, b))
37+
{
38+
return true;
39+
}
40+
41+
if (a is null)
42+
{
43+
return b is null;
44+
}
45+
46+
if (b is null)
47+
{
48+
return false;
49+
}
50+
51+
if (a is IList<TElement> aList && b is IList<TElement> bList)
52+
{
53+
if (aList.Count != bList.Count)
54+
{
55+
return false;
56+
}
57+
58+
for (var i = 0; i < aList.Count; i++)
59+
{
60+
var (el1, el2) = (aList[i], bList[i]);
61+
if (el1 is null)
62+
{
63+
if (el2 is null)
64+
{
65+
continue;
66+
}
67+
68+
return false;
69+
}
70+
71+
if (el2 is null)
72+
{
73+
return false;
74+
}
75+
76+
if (!elementComparer.Equals(el1, el2))
77+
{
78+
return false;
79+
}
80+
}
81+
82+
return true;
83+
}
84+
85+
throw new InvalidOperationException(
86+
CoreStrings.BadListType(
87+
(a is IList<TElement?> ? b : a).GetType().ShortDisplayName(),
88+
typeof(ListComparer<TElement?>).ShortDisplayName(),
89+
typeof(IList<>).MakeGenericType(elementComparer.Type).ShortDisplayName()));
90+
}
91+
92+
private static int GetHashCode(IEnumerable<TElement> source, ValueComparer<TElement> elementComparer)
93+
{
94+
var hash = new HashCode();
95+
96+
foreach (var el in source)
97+
{
98+
hash.Add(el == null ? 0 : elementComparer.GetHashCode(el));
99+
}
100+
101+
return hash.ToHashCode();
102+
}
103+
104+
private static IList<TElement> Snapshot(IEnumerable<TElement> source, ValueComparer<TElement> elementComparer)
105+
{
106+
if (!(source is IList<TElement> sourceList))
107+
{
108+
throw new InvalidOperationException(
109+
CoreStrings.BadListType(
110+
source.GetType().ShortDisplayName(),
111+
typeof(ListComparer<TElement?>).ShortDisplayName(),
112+
typeof(IList<>).MakeGenericType(elementComparer.Type).ShortDisplayName()));
113+
}
114+
115+
if (sourceList.GetType().IsArray)
116+
{
117+
var snapshot = new TElement[sourceList.Count];
118+
119+
for (var i = 0; i < sourceList.Count; i++)
120+
{
121+
var instance = sourceList[i];
122+
if (instance != null)
123+
{
124+
snapshot[i] = elementComparer.Snapshot(instance);
125+
}
126+
}
127+
128+
return snapshot;
129+
}
130+
else
131+
{
132+
var snapshot = source is List<TElement>
133+
? new List<TElement>(sourceList.Count)
134+
: (IList<TElement>)Activator.CreateInstance(source.GetType())!;
135+
136+
foreach (var e in sourceList)
137+
{
138+
snapshot.Add(e == null ? (TElement)(object)null! : elementComparer.Snapshot(e));
139+
}
140+
141+
return snapshot;
142+
}
143+
}
144+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.EntityFrameworkCore.ChangeTracking;
5+
6+
/// <summary>
7+
/// A <see cref="ValueComparer{T}"/> for lists of primitive items. The list can be typed as <see cref="IEnumerable{T}"/>,
8+
/// but can only be used with instances that implement <see cref="IList{T}"/>.
9+
/// </summary>
10+
/// <remarks>
11+
/// <para>
12+
/// This comparer should be used for nullable value types. Use <see cref="NullableValueTypeListComparer{TElement}"/> for reference
13+
/// types and non-nullable value types.
14+
/// </para>
15+
/// <para>
16+
/// See <see href="https://aka.ms/efcore-docs-value-comparers">EF Core value comparers</see> for more information and examples.
17+
/// </para>
18+
/// </remarks>
19+
/// <typeparam name="TElement">The element type.</typeparam>
20+
public sealed class NullableValueTypeListComparer<TElement> : ValueComparer<IEnumerable<TElement?>>
21+
where TElement : struct
22+
{
23+
/// <summary>
24+
/// Creates a new instance of the list comparer.
25+
/// </summary>
26+
/// <param name="elementComparer">The comparer to use for comparing elements.</param>
27+
public NullableValueTypeListComparer(ValueComparer<TElement> elementComparer)
28+
: base(
29+
(a, b) => Compare(a, b, elementComparer),
30+
o => GetHashCode(o, elementComparer),
31+
source => Snapshot(source, elementComparer))
32+
{
33+
}
34+
35+
private static bool Compare(IEnumerable<TElement?>? a, IEnumerable<TElement?>? b, ValueComparer<TElement> elementComparer)
36+
{
37+
if (ReferenceEquals(a, b))
38+
{
39+
return true;
40+
}
41+
42+
if (a is null)
43+
{
44+
return b is null;
45+
}
46+
47+
if (b is null)
48+
{
49+
return false;
50+
}
51+
52+
if (a is IList<TElement?> aList && b is IList<TElement?> bList)
53+
{
54+
if (aList.Count != bList.Count)
55+
{
56+
return false;
57+
}
58+
59+
for (var i = 0; i < aList.Count; i++)
60+
{
61+
var (el1, el2) = (aList[i], bList[i]);
62+
if (el1 is null)
63+
{
64+
if (el2 is null)
65+
{
66+
continue;
67+
}
68+
69+
return false;
70+
}
71+
72+
if (el2 is null)
73+
{
74+
return false;
75+
}
76+
77+
if (!elementComparer.Equals(el1, el2))
78+
{
79+
return false;
80+
}
81+
}
82+
83+
return true;
84+
}
85+
86+
throw new InvalidOperationException(
87+
CoreStrings.BadListType(
88+
(a is IList<TElement?> ? b : a).GetType().ShortDisplayName(),
89+
typeof(NullableValueTypeListComparer<TElement>).ShortDisplayName(),
90+
typeof(IList<>).MakeGenericType(elementComparer.Type).ShortDisplayName()));
91+
}
92+
93+
private static int GetHashCode(IEnumerable<TElement?> source, ValueComparer<TElement> elementComparer)
94+
{
95+
var hash = new HashCode();
96+
97+
foreach (var el in source)
98+
{
99+
hash.Add(el == null ? 0 : elementComparer.GetHashCode(el));
100+
}
101+
102+
return hash.ToHashCode();
103+
}
104+
105+
private static IList<TElement?> Snapshot(IEnumerable<TElement?> source, ValueComparer<TElement> elementComparer)
106+
{
107+
if (!(source is IList<TElement?> sourceList))
108+
{
109+
throw new InvalidOperationException(
110+
CoreStrings.BadListType(
111+
source.GetType().ShortDisplayName(),
112+
typeof(NullableValueTypeListComparer<TElement>).ShortDisplayName(),
113+
typeof(IList<>).MakeGenericType(elementComparer.Type).ShortDisplayName()));
114+
}
115+
116+
if (sourceList.GetType().IsArray)
117+
{
118+
var snapshot = new TElement?[sourceList.Count];
119+
120+
for (var i = 0; i < sourceList.Count; i++)
121+
{
122+
var instance = sourceList[i];
123+
snapshot[i] = instance == null ? null : (TElement?)elementComparer.Snapshot(instance);
124+
}
125+
126+
return snapshot;
127+
}
128+
else
129+
{
130+
var snapshot = source is List<TElement?>
131+
? new List<TElement?>(sourceList.Count)
132+
: (IList<TElement?>)Activator.CreateInstance(source.GetType())!;
133+
134+
foreach (var e in sourceList)
135+
{
136+
snapshot.Add(e == null ? null : (TElement?)elementComparer.Snapshot(e));
137+
}
138+
139+
return snapshot;
140+
}
141+
}
142+
}

src/EFCore/Metadata/Internal/Property.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1151,7 +1151,7 @@ public virtual CoreTypeMapping? TypeMapping
11511151
/// </summary>
11521152
public virtual string? CheckValueComparer(ValueComparer? comparer)
11531153
=> comparer != null
1154-
&& comparer.Type.UnwrapNullableType() != ClrType.UnwrapNullableType()
1154+
&& !comparer.Type.UnwrapNullableType().IsAssignableFrom(ClrType.UnwrapNullableType())
11551155
? CoreStrings.ComparerPropertyMismatch(
11561156
comparer.Type.ShortDisplayName(),
11571157
DeclaringType.DisplayName(),

src/EFCore/Properties/CoreStrings.Designer.cs

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)