Skip to content

Commit

Permalink
Collection change tracking for primitive collections (#31480)
Browse files Browse the repository at this point in the history
  • Loading branch information
ajcvickers committed Aug 17, 2023
1 parent c2c4554 commit a4f0153
Show file tree
Hide file tree
Showing 14 changed files with 1,016 additions and 51 deletions.
3 changes: 2 additions & 1 deletion src/EFCore.Cosmos/Storage/Internal/CosmosTypeMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ protected CosmosTypeMapping(CoreTypeMappingParameters parameters)
/// </summary>
public override CoreTypeMapping Clone(
ValueConverter? converter,
ValueComparer? comparer = null,
CoreTypeMapping? elementMapping = null,
JsonValueReaderWriter? jsonValueReaderWriter = null)
=> new CosmosTypeMapping(Parameters.WithComposedConverter(converter, elementMapping, jsonValueReaderWriter));
=> new CosmosTypeMapping(Parameters.WithComposedConverter(converter, comparer, elementMapping, jsonValueReaderWriter));

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
3 changes: 2 additions & 1 deletion src/EFCore.InMemory/Storage/Internal/InMemoryTypeMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ private InMemoryTypeMapping(CoreTypeMappingParameters parameters)
/// </summary>
public override CoreTypeMapping Clone(
ValueConverter? converter,
ValueComparer? comparer = null,
CoreTypeMapping? elementMapping = null,
JsonValueReaderWriter? jsonValueReaderWriter = null)
=> new InMemoryTypeMapping(Parameters.WithComposedConverter(converter, elementMapping, jsonValueReaderWriter));
=> new InMemoryTypeMapping(Parameters.WithComposedConverter(converter, comparer, elementMapping, jsonValueReaderWriter));

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
7 changes: 5 additions & 2 deletions src/EFCore.Relational/Storage/RelationalTypeMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,15 +230,17 @@ public RelationalTypeMappingParameters WithScale(int? scale)
/// converter composed with any existing converter and set on the new parameter object.
/// </summary>
/// <param name="converter">The converter.</param>
/// <param name="comparer">The comparer.</param>
/// <param name="elementMapping">The element mapping, or <see langword="null" /> for non-collection mappings.</param>
/// <param name="jsonValueReaderWriter">The JSON reader/writer, or <see langword="null" /> to leave unchanged.</param>
/// <returns>The new parameter object.</returns>
public RelationalTypeMappingParameters WithComposedConverter(
ValueConverter? converter,
ValueComparer? comparer,
CoreTypeMapping? elementMapping,
JsonValueReaderWriter? jsonValueReaderWriter)
=> new(
CoreParameters.WithComposedConverter(converter, elementMapping, jsonValueReaderWriter),
CoreParameters.WithComposedConverter(converter, comparer, elementMapping, jsonValueReaderWriter),
StoreType,
StoreTypePostfix,
DbType,
Expand Down Expand Up @@ -428,9 +430,10 @@ public virtual RelationalTypeMapping Clone(int? precision, int? scale)
/// <inheritdoc />
public override CoreTypeMapping Clone(
ValueConverter? converter,
ValueComparer? comparer = null,
CoreTypeMapping? elementMapping = null,
JsonValueReaderWriter? jsonValueReaderWriter = null)
=> Clone(Parameters.WithComposedConverter(converter, elementMapping, jsonValueReaderWriter));
=> Clone(Parameters.WithComposedConverter(converter, comparer, elementMapping, jsonValueReaderWriter));

/// <summary>
/// Clones the type mapping to update facets from the mapping info, if needed.
Expand Down
15 changes: 11 additions & 4 deletions src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,20 +224,27 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo)
Type modelType,
Type? providerType,
CoreTypeMapping? elementMapping)
=> TryFindJsonCollectionMapping(
{
var elementType = modelType.TryGetElementType(typeof(IEnumerable<>))!;

return TryFindJsonCollectionMapping(
info.CoreTypeMappingInfo, modelType, providerType, ref elementMapping, out var collectionReaderWriter)
? (RelationalTypeMapping)FindMapping(
info.WithConverter(
// Note that the converter info is only used temporarily here and never creates an instance.
new ValueConverterInfo(modelType, typeof(string), _ => null!)))!
.Clone(
(ValueConverter)Activator.CreateInstance(
typeof(CollectionToJsonStringConverter<>).MakeGenericType(
modelType.TryGetElementType(typeof(IEnumerable<>))!),
collectionReaderWriter!)!,
typeof(CollectionToJsonStringConverter<>).MakeGenericType(elementType), collectionReaderWriter!)!,
(ValueComparer?)Activator.CreateInstance(
elementType.IsNullableValueType()
? typeof(NullableValueTypeListComparer<>).MakeGenericType(elementType.UnwrapNullableType())
: typeof(ListComparer<>).MakeGenericType(elementMapping!.Comparer.Type),
elementMapping!.Comparer),
elementMapping,
collectionReaderWriter)
: null;
}

/// <summary>
/// Finds the type mapping for a given <see cref="IProperty" />.
Expand Down
144 changes: 144 additions & 0 deletions src/EFCore/ChangeTracking/ListComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore.ChangeTracking;

/// <summary>
/// A <see cref="ValueComparer{T}"/> for lists of primitive items. The list can be typed as <see cref="IEnumerable{T}"/>,
/// but can only be used with instances that implement <see cref="IList{T}"/>.
/// </summary>
/// <remarks>
/// <para>
/// This comparer should be used for reference types and non-nullable value types. Use
/// <see cref="NullableValueTypeListComparer{TElement}"/> for nullable value types.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-value-comparers">EF Core value comparers</see> for more information and examples.
/// </para>
/// </remarks>
/// <typeparam name="TElement">The element type.</typeparam>
public sealed class ListComparer<TElement> : ValueComparer<IEnumerable<TElement>>
{
/// <summary>
/// Creates a new instance of the list comparer.
/// </summary>
/// <param name="elementComparer">The comparer to use for comparing elements.</param>
public ListComparer(ValueComparer<TElement> elementComparer)
: base(
(a, b) => Compare(a, b, elementComparer),
o => GetHashCode(o, elementComparer),
source => Snapshot(source, elementComparer))
{
}

private static bool Compare(IEnumerable<TElement>? a, IEnumerable<TElement>? b, ValueComparer<TElement> elementComparer)
{
if (ReferenceEquals(a, b))
{
return true;
}

if (a is null)
{
return b is null;
}

if (b is null)
{
return false;
}

if (a is IList<TElement> aList && b is IList<TElement> bList)
{
if (aList.Count != bList.Count)
{
return false;
}

for (var i = 0; i < aList.Count; i++)
{
var (el1, el2) = (aList[i], bList[i]);
if (el1 is null)
{
if (el2 is null)
{
continue;
}

return false;
}

if (el2 is null)
{
return false;
}

if (!elementComparer.Equals(el1, el2))
{
return false;
}
}

return true;
}

throw new InvalidOperationException(
CoreStrings.BadListType(
(a is IList<TElement?> ? b : a).GetType().ShortDisplayName(),
typeof(ListComparer<TElement?>).ShortDisplayName(),
typeof(IList<>).MakeGenericType(elementComparer.Type).ShortDisplayName()));
}

private static int GetHashCode(IEnumerable<TElement> source, ValueComparer<TElement> elementComparer)
{
var hash = new HashCode();

foreach (var el in source)
{
hash.Add(el == null ? 0 : elementComparer.GetHashCode(el));
}

return hash.ToHashCode();
}

private static IList<TElement> Snapshot(IEnumerable<TElement> source, ValueComparer<TElement> elementComparer)
{
if (!(source is IList<TElement> sourceList))
{
throw new InvalidOperationException(
CoreStrings.BadListType(
source.GetType().ShortDisplayName(),
typeof(ListComparer<TElement?>).ShortDisplayName(),
typeof(IList<>).MakeGenericType(elementComparer.Type).ShortDisplayName()));
}

if (sourceList.IsReadOnly)
{
var snapshot = new TElement[sourceList.Count];

for (var i = 0; i < sourceList.Count; i++)
{
var instance = sourceList[i];
if (instance != null)
{
snapshot[i] = elementComparer.Snapshot(instance);
}
}

return snapshot;
}
else
{
var snapshot = (source is List<TElement> || sourceList.IsReadOnly)
? new List<TElement>(sourceList.Count)
: (IList<TElement>)Activator.CreateInstance(source.GetType())!;

foreach (var e in sourceList)
{
snapshot.Add(e == null ? (TElement)(object)null! : elementComparer.Snapshot(e));
}

return snapshot;
}
}
}
142 changes: 142 additions & 0 deletions src/EFCore/ChangeTracking/NullableValueTypeListComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore.ChangeTracking;

/// <summary>
/// A <see cref="ValueComparer{T}"/> for lists of primitive items. The list can be typed as <see cref="IEnumerable{T}"/>,
/// but can only be used with instances that implement <see cref="IList{T}"/>.
/// </summary>
/// <remarks>
/// <para>
/// This comparer should be used for nullable value types. Use <see cref="NullableValueTypeListComparer{TElement}"/> for reference
/// types and non-nullable value types.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-value-comparers">EF Core value comparers</see> for more information and examples.
/// </para>
/// </remarks>
/// <typeparam name="TElement">The element type.</typeparam>
public sealed class NullableValueTypeListComparer<TElement> : ValueComparer<IEnumerable<TElement?>>
where TElement : struct
{
/// <summary>
/// Creates a new instance of the list comparer.
/// </summary>
/// <param name="elementComparer">The comparer to use for comparing elements.</param>
public NullableValueTypeListComparer(ValueComparer<TElement> elementComparer)
: base(
(a, b) => Compare(a, b, elementComparer),
o => GetHashCode(o, elementComparer),
source => Snapshot(source, elementComparer))
{
}

private static bool Compare(IEnumerable<TElement?>? a, IEnumerable<TElement?>? b, ValueComparer<TElement> elementComparer)
{
if (ReferenceEquals(a, b))
{
return true;
}

if (a is null)
{
return b is null;
}

if (b is null)
{
return false;
}

if (a is IList<TElement?> aList && b is IList<TElement?> bList)
{
if (aList.Count != bList.Count)
{
return false;
}

for (var i = 0; i < aList.Count; i++)
{
var (el1, el2) = (aList[i], bList[i]);
if (el1 is null)
{
if (el2 is null)
{
continue;
}

return false;
}

if (el2 is null)
{
return false;
}

if (!elementComparer.Equals(el1, el2))
{
return false;
}
}

return true;
}

throw new InvalidOperationException(
CoreStrings.BadListType(
(a is IList<TElement?> ? b : a).GetType().ShortDisplayName(),
typeof(NullableValueTypeListComparer<TElement>).ShortDisplayName(),
typeof(IList<>).MakeGenericType(elementComparer.Type.MakeNullable()).ShortDisplayName()));
}

private static int GetHashCode(IEnumerable<TElement?> source, ValueComparer<TElement> elementComparer)
{
var hash = new HashCode();

foreach (var el in source)
{
hash.Add(el == null ? 0 : elementComparer.GetHashCode(el));
}

return hash.ToHashCode();
}

private static IList<TElement?> Snapshot(IEnumerable<TElement?> source, ValueComparer<TElement> elementComparer)
{
if (!(source is IList<TElement?> sourceList))
{
throw new InvalidOperationException(
CoreStrings.BadListType(
source.GetType().ShortDisplayName(),
typeof(NullableValueTypeListComparer<TElement>).ShortDisplayName(),
typeof(IList<>).MakeGenericType(elementComparer.Type.MakeNullable()).ShortDisplayName()));
}

if (sourceList.IsReadOnly)
{
var snapshot = new TElement?[sourceList.Count];

for (var i = 0; i < sourceList.Count; i++)
{
var instance = sourceList[i];
snapshot[i] = instance == null ? null : (TElement?)elementComparer.Snapshot(instance);
}

return snapshot;
}
else
{
var snapshot = source is List<TElement?> || sourceList.IsReadOnly
? new List<TElement?>(sourceList.Count)
: (IList<TElement?>)Activator.CreateInstance(source.GetType())!;

foreach (var e in sourceList)
{
snapshot.Add(e == null ? null : (TElement?)elementComparer.Snapshot(e));
}

return snapshot;
}
}
}
2 changes: 1 addition & 1 deletion src/EFCore/Metadata/Internal/Property.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1151,7 +1151,7 @@ public virtual CoreTypeMapping? TypeMapping
/// </summary>
public virtual string? CheckValueComparer(ValueComparer? comparer)
=> comparer != null
&& comparer.Type.UnwrapNullableType() != ClrType.UnwrapNullableType()
&& !comparer.Type.UnwrapNullableType().IsAssignableFrom(ClrType.UnwrapNullableType())
? CoreStrings.ComparerPropertyMismatch(
comparer.Type.ShortDisplayName(),
DeclaringType.DisplayName(),
Expand Down
8 changes: 8 additions & 0 deletions src/EFCore/Properties/CoreStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit a4f0153

Please sign in to comment.