Skip to content

Commit

Permalink
Better handling of nullable element comparers (#31672)
Browse files Browse the repository at this point in the history
  • Loading branch information
ajcvickers authored Sep 12, 2023
1 parent 88edb55 commit 99c8119
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 73 deletions.
38 changes: 13 additions & 25 deletions src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Concurrent;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.CodeAnalysis;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;

namespace Microsoft.EntityFrameworkCore.Storage;

Expand Down Expand Up @@ -213,45 +214,32 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo)
this);

/// <summary>
/// Attempts to find a type mapping for a collection of primitive types.
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
/// <param name="info">The mapping info being used.</param>
/// <param name="modelType">The model type.</param>
/// <param name="providerType">The provider type.</param>
/// <param name="elementMapping">The element mapping, if known.</param>
/// <returns>The type mapping, or <see langword="null" /> if none was found.</returns>
[EntityFrameworkInternal]
protected virtual RelationalTypeMapping? FindCollectionMapping(
RelationalTypeMappingInfo info,
Type modelType,
Type? providerType,
CoreTypeMapping? elementMapping)
{
if (TryFindJsonCollectionMapping(
info.CoreTypeMappingInfo, modelType, providerType, ref elementMapping, out var collectionReaderWriter))
{
var elementType = modelType.TryGetElementType(typeof(IEnumerable<>))!;

var comparer = (ValueComparer?)Activator.CreateInstance(
elementType.IsNullableValueType()
? typeof(NullableValueTypeListComparer<>).MakeGenericType(elementType.UnwrapNullableType())
: typeof(ListComparer<>).MakeGenericType(elementMapping!.Comparer.Type),
elementMapping!.Comparer);

return (RelationalTypeMapping)FindMapping(
=> TryFindJsonCollectionMapping(
info.CoreTypeMappingInfo, modelType, providerType, ref elementMapping, out var comparer, 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!)))!
.WithComposedConverter(
(ValueConverter)Activator.CreateInstance(
typeof(CollectionToJsonStringConverter<>).MakeGenericType(elementType), collectionReaderWriter!)!,
typeof(CollectionToJsonStringConverter<>).MakeGenericType(
modelType.TryGetElementType(typeof(IEnumerable<>))!), collectionReaderWriter!)!,
comparer,
comparer,
elementMapping,
collectionReaderWriter);
}

return null;
}
collectionReaderWriter)
: null;

/// <summary>
/// Finds the type mapping for a given <see cref="IProperty" />.
Expand Down
27 changes: 24 additions & 3 deletions src/EFCore/ChangeTracking/ListComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,14 @@ public ListComparer(ValueComparer<TElement> elementComparer)
{
}

private static bool Compare(IEnumerable<TElement>? a, IEnumerable<TElement>? b, ValueComparer<TElement> elementComparer)
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[EntityFrameworkInternal]
public static bool Compare(IEnumerable<TElement>? a, IEnumerable<TElement>? b, ValueComparer<TElement> elementComparer)
{
if (ReferenceEquals(a, b))
{
Expand Down Expand Up @@ -89,7 +96,14 @@ private static bool Compare(IEnumerable<TElement>? a, IEnumerable<TElement>? b,
typeof(IList<>).MakeGenericType(elementComparer.Type).ShortDisplayName()));
}

private static int GetHashCode(IEnumerable<TElement> source, ValueComparer<TElement> elementComparer)
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[EntityFrameworkInternal]
public static int GetHashCode(IEnumerable<TElement> source, ValueComparer<TElement> elementComparer)
{
var hash = new HashCode();

Expand All @@ -101,7 +115,14 @@ private static int GetHashCode(IEnumerable<TElement> source, ValueComparer<TElem
return hash.ToHashCode();
}

private static IList<TElement> Snapshot(IEnumerable<TElement> source, ValueComparer<TElement> elementComparer)
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[EntityFrameworkInternal]
public static IList<TElement> Snapshot(IEnumerable<TElement> source, ValueComparer<TElement> elementComparer)
{
if (source is not IList<TElement> sourceList)
{
Expand Down
33 changes: 27 additions & 6 deletions src/EFCore/ChangeTracking/NullableValueTypeListComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,22 @@ public sealed class NullableValueTypeListComparer<TElement> : ValueComparer<IEnu
/// 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)
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)
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[EntityFrameworkInternal]
public static bool Compare(IEnumerable<TElement?>? a, IEnumerable<TElement?>? b, ValueComparer<TElement?> elementComparer)
{
if (ReferenceEquals(a, b))
{
Expand Down Expand Up @@ -90,7 +97,14 @@ private static bool Compare(IEnumerable<TElement?>? a, IEnumerable<TElement?>? b
typeof(IList<>).MakeGenericType(elementComparer.Type.MakeNullable()).ShortDisplayName()));
}

private static int GetHashCode(IEnumerable<TElement?> source, ValueComparer<TElement> elementComparer)
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[EntityFrameworkInternal]
public static int GetHashCode(IEnumerable<TElement?> source, ValueComparer<TElement?> elementComparer)
{
var hash = new HashCode();

Expand All @@ -102,7 +116,14 @@ private static int GetHashCode(IEnumerable<TElement?> source, ValueComparer<TEle
return hash.ToHashCode();
}

private static IList<TElement?> Snapshot(IEnumerable<TElement?> source, ValueComparer<TElement> elementComparer)
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[EntityFrameworkInternal]
public static IList<TElement?> Snapshot(IEnumerable<TElement?> source, ValueComparer<TElement?> elementComparer)
{
if (source is not IList<TElement?> sourceList)
{
Expand All @@ -120,7 +141,7 @@ private static int GetHashCode(IEnumerable<TElement?> source, ValueComparer<TEle
for (var i = 0; i < sourceList.Count; i++)
{
var instance = sourceList[i];
snapshot[i] = instance == null ? null : (TElement?)elementComparer.Snapshot(instance);
snapshot[i] = instance == null ? null : elementComparer.Snapshot(instance);
}

return snapshot;
Expand All @@ -133,7 +154,7 @@ private static int GetHashCode(IEnumerable<TElement?> source, ValueComparer<TEle

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

return snapshot;
Expand Down
164 changes: 164 additions & 0 deletions src/EFCore/ChangeTracking/ObjectListComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// 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 when the element of the comparer is typed as <see cref="object"/>.
/// </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 ObjectListComparer<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 ObjectListComparer(ValueComparer elementComparer)
: base(
(a, b) => Compare(a, b, elementComparer),
o => GetHashCode(o, elementComparer),
source => Snapshot(source, elementComparer))
{
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[EntityFrameworkInternal]
public static bool Compare(IEnumerable<TElement>? a, IEnumerable<TElement>? b, ValueComparer 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<object?> aList && b is IList<object?> 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()));
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[EntityFrameworkInternal]
public static int GetHashCode(IEnumerable<TElement> source, ValueComparer elementComparer)
{
var hash = new HashCode();

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

return hash.ToHashCode();
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[EntityFrameworkInternal]
public static IList<TElement> Snapshot(IEnumerable<TElement> source, ValueComparer elementComparer)
{
if (source is not 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] = (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 ? (TElement)(object?)null! : (TElement)elementComparer.Snapshot(e));
}

return snapshot;
}
}
}
Loading

0 comments on commit 99c8119

Please sign in to comment.