diff --git a/src/EFCore.Cosmos/ChangeTracking/Internal/ListComparer.cs b/src/EFCore.Cosmos/ChangeTracking/Internal/ListComparer.cs
new file mode 100644
index 00000000000..f55d5ea3891
--- /dev/null
+++ b/src/EFCore.Cosmos/ChangeTracking/Internal/ListComparer.cs
@@ -0,0 +1,101 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+
+namespace Microsoft.EntityFrameworkCore.Cosmos.ChangeTracking.Internal
+{
+ ///
+ /// 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.
+ ///
+ public sealed class ListComparer : ValueComparer
+ where TCollection : class, IEnumerable
+ {
+ ///
+ /// 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.
+ ///
+ public ListComparer(ValueComparer elementComparer, bool readOnly)
+ : base(
+ (a, b) => Compare(a, b, (ValueComparer)elementComparer),
+ o => GetHashCode(o, (ValueComparer)elementComparer),
+ source => Snapshot(source, (ValueComparer)elementComparer, readOnly))
+ {
+ }
+
+ ///
+ /// 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.
+ ///
+ public override Type Type => typeof(TCollection);
+
+ private static bool Compare(TCollection? a, TCollection? b, ValueComparer elementComparer)
+ {
+ if (a is not IReadOnlyList aList)
+ {
+ return b is not IReadOnlyList;
+ }
+
+ if (b is not IReadOnlyList bList || aList.Count != bList.Count)
+ {
+ return false;
+ }
+
+ if (aList == bList)
+ {
+ return true;
+ }
+
+ for (var i = 0; i < aList.Count; i++)
+ {
+ if (!elementComparer.Equals(aList[i], bList[i]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static int GetHashCode(TCollection source, ValueComparer elementComparer)
+ {
+ var hash = new HashCode();
+ foreach (var el in source)
+ {
+ hash.Add(el, elementComparer);
+ }
+
+ return hash.ToHashCode();
+ }
+
+ private static TCollection? Snapshot(TCollection? source, ValueComparer elementComparer, bool readOnly)
+ {
+ if (source == null)
+ {
+ return null;
+ }
+
+ if (readOnly)
+ {
+ return source;
+ }
+
+ var snapshot = new List(((IReadOnlyList)source).Count);
+ foreach (var e in source)
+ {
+ snapshot.Add(elementComparer.Snapshot(e)!);
+ }
+
+ return (TCollection)(object)snapshot;
+ }
+ }
+}
diff --git a/src/EFCore.Cosmos/ChangeTracking/Internal/NullableEqualityComparer.cs b/src/EFCore.Cosmos/ChangeTracking/Internal/NullableEqualityComparer.cs
new file mode 100644
index 00000000000..5ee02748849
--- /dev/null
+++ b/src/EFCore.Cosmos/ChangeTracking/Internal/NullableEqualityComparer.cs
@@ -0,0 +1,48 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace Microsoft.EntityFrameworkCore.Cosmos.ChangeTracking.Internal
+{
+ ///
+ /// 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.
+ ///
+ public class NullableEqualityComparer : IEqualityComparer
+ where T : struct
+ {
+ private readonly IEqualityComparer _underlyingComparer;
+
+ ///
+ /// 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.
+ ///
+ public NullableEqualityComparer(IEqualityComparer underlyingComparer)
+ => _underlyingComparer = underlyingComparer;
+
+ ///
+ /// 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.
+ ///
+ public virtual bool Equals(T? x, T? y)
+ => x is null
+ ? y is null
+ : y.HasValue && _underlyingComparer.Equals(x.Value, y.Value);
+
+ ///
+ /// 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.
+ ///
+ public virtual int GetHashCode(T? obj)
+ => obj is null ? 0 : _underlyingComparer.GetHashCode(obj.Value);
+ }
+}
diff --git a/src/EFCore.Cosmos/ChangeTracking/Internal/NullableListComparer.cs b/src/EFCore.Cosmos/ChangeTracking/Internal/NullableListComparer.cs
new file mode 100644
index 00000000000..b1582cc32b1
--- /dev/null
+++ b/src/EFCore.Cosmos/ChangeTracking/Internal/NullableListComparer.cs
@@ -0,0 +1,112 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+
+namespace Microsoft.EntityFrameworkCore.Cosmos.ChangeTracking.Internal
+{
+ ///
+ /// 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.
+ ///
+ public sealed class NullableListComparer : ValueComparer
+ where TCollection : class, IEnumerable
+ where TElement : struct
+ {
+ ///
+ /// 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.
+ ///
+ public NullableListComparer(ValueComparer elementComparer, bool readOnly)
+ : base(
+ (a, b) => Compare(a, b, (ValueComparer)elementComparer),
+ o => GetHashCode(o, (ValueComparer)elementComparer),
+ source => Snapshot(source, (ValueComparer)elementComparer, readOnly))
+ { }
+
+ ///
+ /// 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.
+ ///
+ public override Type Type => typeof(TCollection);
+
+ private static bool Compare(TCollection? a, TCollection? b, ValueComparer elementComparer)
+ {
+ if (a is not IReadOnlyList aList)
+ {
+ return b is not IReadOnlyList;
+ }
+
+ if (b is not IReadOnlyList bList || aList.Count != bList.Count)
+ {
+ return false;
+ }
+
+ if (aList == bList)
+ {
+ return true;
+ }
+
+ for (var i = 0; i < aList.Count; i++)
+ {
+ var (aElement, bElement) = (aList[i], bList[i]);
+ if (aElement is null)
+ {
+ if (bElement is null)
+ {
+ continue;
+ }
+
+ return false;
+ }
+ if (bElement is null || !elementComparer.Equals(aElement, bElement))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static int GetHashCode(TCollection source, ValueComparer elementComparer)
+ {
+ var nullableEqualityComparer = new NullableEqualityComparer(elementComparer);
+ var hash = new HashCode();
+ foreach (var el in source)
+ {
+ hash.Add(el, nullableEqualityComparer);
+ }
+
+ return hash.ToHashCode();
+ }
+
+ private static TCollection? Snapshot(TCollection? source, ValueComparer elementComparer, bool readOnly)
+ {
+ if (source == null)
+ {
+ return null;
+ }
+
+ if (readOnly)
+ {
+ return source;
+ }
+
+ var snapshot = new List(((IReadOnlyList)source).Count);
+ foreach (var e in source)
+ {
+ snapshot.Add(e is { } value ? elementComparer.Snapshot(value) : null);
+ }
+
+ return (TCollection)(object)snapshot;
+ }
+ }
+}
diff --git a/src/EFCore.Cosmos/ChangeTracking/Internal/NullableSingleDimensionalArrayComparer.cs b/src/EFCore.Cosmos/ChangeTracking/Internal/NullableSingleDimensionalArrayComparer.cs
new file mode 100644
index 00000000000..04f6d950605
--- /dev/null
+++ b/src/EFCore.Cosmos/ChangeTracking/Internal/NullableSingleDimensionalArrayComparer.cs
@@ -0,0 +1,101 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+
+namespace Microsoft.EntityFrameworkCore.Cosmos.ChangeTracking.Internal
+{
+ ///
+ /// 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.
+ ///
+ public sealed class NullableSingleDimensionalArrayComparer : ValueComparer
+ where TElement : struct
+ {
+ ///
+ /// 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.
+ ///
+ public NullableSingleDimensionalArrayComparer(ValueComparer elementComparer) : base(
+ (a, b) => Compare(a, b, (ValueComparer)elementComparer),
+ o => GetHashCode(o, (ValueComparer)elementComparer),
+ source => Snapshot(source, (ValueComparer)elementComparer))
+ { }
+
+ ///
+ /// 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.
+ ///
+ public override Type Type => typeof(TElement?[]);
+
+ private static bool Compare(TElement?[]? a, TElement?[]? b, ValueComparer elementComparer)
+ {
+ if (a is null)
+ {
+ return b is null;
+ }
+
+ if (b is null || a.Length != b.Length)
+ {
+ return false;
+ }
+
+ for (var i = 0; i < a.Length; i++)
+ {
+ var (aElement, bElement) = (a[i], b[i]);
+ if (aElement is null)
+ {
+ if (bElement is null)
+ {
+ continue;
+ }
+
+ return false;
+ }
+ if (bElement is null || !elementComparer.Equals(aElement, bElement))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static int GetHashCode(TElement?[] source, ValueComparer elementComparer)
+ {
+ var nullableEqualityComparer = new NullableEqualityComparer(elementComparer);
+ var hash = new HashCode();
+ foreach (var el in source)
+ {
+ hash.Add(el, nullableEqualityComparer);
+ }
+
+ return hash.ToHashCode();
+ }
+
+ [return: NotNullIfNotNull("source")]
+ private static TElement?[]? Snapshot(TElement?[]? source, ValueComparer elementComparer)
+ {
+ if (source == null)
+ {
+ return null;
+ }
+
+ var snapshot = new TElement?[source.Length];
+ for (var i = 0; i < source.Length; i++)
+ {
+ snapshot[i] = source[i] is { } value ? elementComparer.Snapshot(value) : null;
+ }
+
+ return snapshot;
+ }
+ }
+}
diff --git a/src/EFCore.Cosmos/ChangeTracking/Internal/NullableStringDictionaryComparer.cs b/src/EFCore.Cosmos/ChangeTracking/Internal/NullableStringDictionaryComparer.cs
new file mode 100644
index 00000000000..5f3c4ae58be
--- /dev/null
+++ b/src/EFCore.Cosmos/ChangeTracking/Internal/NullableStringDictionaryComparer.cs
@@ -0,0 +1,119 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+
+namespace Microsoft.EntityFrameworkCore.Cosmos.ChangeTracking.Internal
+{
+ ///
+ /// 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.
+ ///
+ public sealed class NullableStringDictionaryComparer : ValueComparer
+ where TCollection : class, IEnumerable>
+ where TElement : struct
+ {
+ ///
+ /// 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.
+ ///
+ public NullableStringDictionaryComparer(ValueComparer elementComparer, bool readOnly)
+ : base(
+ (a, b) => Compare(a, b, (ValueComparer)elementComparer),
+ o => GetHashCode(o, (ValueComparer)elementComparer),
+ source => Snapshot(source, (ValueComparer)elementComparer, readOnly))
+ {
+ }
+
+ ///
+ /// 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.
+ ///
+ public override Type Type => typeof(TCollection);
+
+ private static bool Compare(TCollection? a, TCollection? b, ValueComparer elementComparer)
+ {
+ if (a is not IReadOnlyDictionary aDict)
+ {
+ return b is not IReadOnlyDictionary;
+ }
+
+ if (b is not IReadOnlyDictionary bDict || aDict.Count != bDict.Count)
+ {
+ return false;
+ }
+
+ if (aDict == bDict)
+ {
+ return true;
+ }
+
+ foreach (var aPair in aDict)
+ {
+ if (!bDict.TryGetValue(aPair.Key, out var bValue))
+ {
+ return false;
+ }
+
+ if (aPair.Value is null)
+ {
+ if (bValue is null)
+ {
+ continue;
+ }
+
+ return false;
+ }
+
+ if (bValue is null || !elementComparer.Equals(aPair.Value, bValue))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static int GetHashCode(TCollection source, ValueComparer elementComparer)
+ {
+ var nullableEqualityComparer = new NullableEqualityComparer(elementComparer);
+ var hash = new HashCode();
+ foreach (var el in source)
+ {
+ hash.Add(el.Key);
+ hash.Add(el.Value, nullableEqualityComparer);
+ }
+
+ return hash.ToHashCode();
+ }
+
+ private static TCollection? Snapshot(TCollection? source, ValueComparer elementComparer, bool readOnly)
+ {
+ if (source == null)
+ {
+ return null;
+ }
+
+ if (readOnly)
+ {
+ return source;
+ }
+
+ var snapshot = new Dictionary(((IReadOnlyDictionary)source).Count);
+ foreach (var e in source)
+ {
+ snapshot.Add(e.Key, e.Value is null ? null : elementComparer.Snapshot(e.Value.Value));
+ }
+
+ return (TCollection)(object)snapshot;
+ }
+ }
+}
diff --git a/src/EFCore.Cosmos/ChangeTracking/Internal/SingleDimensionalArrayComparer.cs b/src/EFCore.Cosmos/ChangeTracking/Internal/SingleDimensionalArrayComparer.cs
new file mode 100644
index 00000000000..bf195dc1169
--- /dev/null
+++ b/src/EFCore.Cosmos/ChangeTracking/Internal/SingleDimensionalArrayComparer.cs
@@ -0,0 +1,88 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+
+namespace Microsoft.EntityFrameworkCore.Cosmos.ChangeTracking.Internal
+{
+ ///
+ /// 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.
+ ///
+ public sealed class SingleDimensionalArrayComparer : ValueComparer
+ {
+ ///
+ /// 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.
+ ///
+ public SingleDimensionalArrayComparer(ValueComparer elementComparer) : base(
+ (a, b) => Compare(a, b, (ValueComparer)elementComparer),
+ o => GetHashCode(o, (ValueComparer)elementComparer),
+ source => Snapshot(source, (ValueComparer)elementComparer))
+ { }
+
+ ///
+ /// 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.
+ ///
+ public override Type Type => typeof(TElement[]);
+
+ private static bool Compare(TElement[]? a, TElement[]? b, ValueComparer elementComparer)
+ {
+ if (a is null)
+ {
+ return b is null;
+ }
+
+ if (b is null || a.Length != b.Length)
+ {
+ return false;
+ }
+
+ for (var i = 0; i < a.Length; i++)
+ {
+ if (!elementComparer.Equals(a[i], b[i]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static int GetHashCode(TElement[] source, ValueComparer elementComparer)
+ {
+ var hash = new HashCode();
+ foreach (var el in source)
+ {
+ hash.Add(el, elementComparer);
+ }
+
+ return hash.ToHashCode();
+ }
+
+ [return: NotNullIfNotNull("source")]
+ private static TElement[]? Snapshot(TElement[]? source, ValueComparer elementComparer)
+ {
+ if (source == null)
+ {
+ return null;
+ }
+
+ var snapshot = new TElement[source.Length];
+ for (var i = 0; i < source.Length; i++)
+ {
+ snapshot[i] = elementComparer.Snapshot(source[i])!;
+ }
+ return snapshot;
+ }
+ }
+}
diff --git a/src/EFCore.Cosmos/ChangeTracking/Internal/StringDictionaryComparer.cs b/src/EFCore.Cosmos/ChangeTracking/Internal/StringDictionaryComparer.cs
new file mode 100644
index 00000000000..caa08e52258
--- /dev/null
+++ b/src/EFCore.Cosmos/ChangeTracking/Internal/StringDictionaryComparer.cs
@@ -0,0 +1,107 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+
+namespace Microsoft.EntityFrameworkCore.Cosmos.ChangeTracking.Internal
+{
+ ///
+ /// 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.
+ ///
+ public sealed class StringDictionaryComparer : ValueComparer
+ where TCollection : class, IEnumerable>
+ {
+ ///
+ /// 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.
+ ///
+ public StringDictionaryComparer(ValueComparer elementComparer, bool readOnly)
+ : base(
+ (a, b) => Compare(a, b, (ValueComparer)elementComparer),
+ o => GetHashCode(o, (ValueComparer)elementComparer),
+ source => Snapshot(source, (ValueComparer)elementComparer, readOnly))
+ {
+ }
+
+ ///
+ /// 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.
+ ///
+ public override Type Type => typeof(TCollection);
+
+ private static bool Compare(TCollection? a, TCollection? b, ValueComparer elementComparer)
+ {
+ if (a is not IReadOnlyDictionary aDict)
+ {
+ return b is not IReadOnlyDictionary;
+ }
+
+ if (b is not IReadOnlyDictionary bDict || aDict.Count != bDict.Count)
+ {
+ return false;
+ }
+
+ if (aDict == bDict)
+ {
+ return true;
+ }
+
+ foreach (var aPair in aDict)
+ {
+ if (!bDict.TryGetValue(aPair.Key, out var bValue))
+ {
+ return false;
+ }
+
+ if (!elementComparer.Equals(aPair.Value, bValue))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static int GetHashCode(TCollection source, ValueComparer elementComparer)
+ {
+ var hash = new HashCode();
+ foreach (var el in source)
+ {
+ hash.Add(el.Key);
+ hash.Add(el.Value, elementComparer);
+ }
+
+ return hash.ToHashCode();
+ }
+
+ private static TCollection? Snapshot(TCollection? source, ValueComparer elementComparer, bool readOnly)
+ {
+ if (source == null)
+ {
+ return null;
+ }
+
+ if (readOnly)
+ {
+ return source;
+ }
+
+ var snapshot = new Dictionary(((IReadOnlyDictionary)source).Count);
+ foreach (var e in source)
+ {
+ snapshot.Add(e.Key, elementComparer.Snapshot(e.Value)!);
+ }
+
+ return (TCollection)(object)snapshot;
+ }
+ }
+}
diff --git a/src/EFCore.Cosmos/Metadata/Conventions/CosmosInversePropertyAttributeConvention.cs b/src/EFCore.Cosmos/Metadata/Conventions/CosmosInversePropertyAttributeConvention.cs
index 0d5bf5164d1..ac5f1f8bc4b 100644
--- a/src/EFCore.Cosmos/Metadata/Conventions/CosmosInversePropertyAttributeConvention.cs
+++ b/src/EFCore.Cosmos/Metadata/Conventions/CosmosInversePropertyAttributeConvention.cs
@@ -46,7 +46,7 @@ public CosmosInversePropertyAttributeConvention(ProviderConventionSetBuilderDepe
targetClrType,
navigationMemberInfo,
shouldCreate ? ConfigurationSource.DataAnnotation : null,
- targetShouldBeOwned: true);
+ CosmosRelationshipDiscoveryConvention.ShouldBeOwnedType(targetClrType, entityTypeBuilder.Metadata.Model));
#pragma warning restore EF1001 // Internal EF Core API usage.
}
}
diff --git a/src/EFCore.Cosmos/Metadata/Conventions/CosmosRelationshipDiscoveryConvention.cs b/src/EFCore.Cosmos/Metadata/Conventions/CosmosRelationshipDiscoveryConvention.cs
index ca95450527d..10a660c8c9e 100644
--- a/src/EFCore.Cosmos/Metadata/Conventions/CosmosRelationshipDiscoveryConvention.cs
+++ b/src/EFCore.Cosmos/Metadata/Conventions/CosmosRelationshipDiscoveryConvention.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;
// ReSharper disable once CheckNamespace
@@ -30,6 +31,15 @@ public CosmosRelationshipDiscoveryConvention(ProviderConventionSetBuilderDepende
/// The model.
/// if the given entity type should be owned.
protected override bool? ShouldBeOwned(Type targetType, IConventionModel model)
- => true;
+ => ShouldBeOwnedType(targetType, model);
+
+ ///
+ /// Returns a value indicating whether the given entity type should be added as owned if it isn't currently in the model.
+ ///
+ /// Target entity type.
+ /// The model.
+ /// if the given entity type should be owned.
+ public static bool ShouldBeOwnedType(Type targetType, IConventionModel model)
+ => !targetType.IsGenericType || targetType.GetGenericTypeDefinition() != typeof(List<>);
}
}
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
index 1755aea378a..fce71a4dfc9 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.ChangeTracking;
+using Microsoft.EntityFrameworkCore.Cosmos.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Utilities;
using Newtonsoft.Json.Linq;
@@ -32,7 +33,6 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies)
_clrTypeMappings
= new Dictionary
{
- { typeof(byte[]), new CosmosTypeMapping(typeof(byte[]), keyComparer: new ArrayStructuralComparer()) },
{ typeof(JObject), new CosmosTypeMapping(typeof(JObject)) }
};
}
@@ -48,11 +48,16 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies)
var clrType = mappingInfo.ClrType;
Check.DebugAssert(clrType != null, "ClrType is null");
- if (_clrTypeMappings.TryGetValue(clrType, out var mapping))
- {
- return mapping;
- }
+ return _clrTypeMappings.TryGetValue(clrType, out var mapping)
+ ? mapping
+ : FindPrimitiveMapping(mappingInfo)
+ ?? FindCollectionMapping(mappingInfo)
+ ?? base.FindMapping(mappingInfo);
+ }
+ private CoreTypeMapping? FindPrimitiveMapping(in TypeMappingInfo mappingInfo)
+ {
+ var clrType = mappingInfo.ClrType!;
if ((clrType.IsValueType
&& !clrType.IsEnum)
|| clrType == typeof(string))
@@ -60,7 +65,103 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies)
return new CosmosTypeMapping(clrType);
}
- return base.FindMapping(mappingInfo);
+ return null;
+ }
+
+ private CoreTypeMapping? FindCollectionMapping(in TypeMappingInfo mappingInfo)
+ {
+ var clrType = mappingInfo.ClrType!;
+ var elementType = clrType.TryGetSequenceType();
+ if (elementType == null)
+ {
+ return null;
+ }
+
+ if (clrType.IsArray)
+ {
+ var elementMapping = FindPrimitiveMapping(new TypeMappingInfo(elementType));
+ return elementMapping == null
+ ? null
+ : new CosmosTypeMapping(clrType, CreateArrayComparer(elementMapping, elementType));
+ }
+
+ if (clrType.IsGenericType
+ && !clrType.IsGenericTypeDefinition)
+ {
+ var genericTypeDefinition = clrType.GetGenericTypeDefinition();
+ if (genericTypeDefinition == typeof(List<>)
+ || genericTypeDefinition == typeof(IList<>)
+ || genericTypeDefinition == typeof(IReadOnlyList<>))
+ {
+ var elementMapping = FindPrimitiveMapping(new TypeMappingInfo(elementType));
+ return elementMapping == null
+ ? null
+ : new CosmosTypeMapping(clrType,
+ CreateListComparer(elementMapping, elementType, clrType, genericTypeDefinition == typeof(IReadOnlyList<>)));
+ }
+
+ if (genericTypeDefinition == typeof(Dictionary<,>)
+ || genericTypeDefinition == typeof(IDictionary<,>)
+ || genericTypeDefinition == typeof(IReadOnlyDictionary<,>))
+ {
+ var genericArguments = clrType.GenericTypeArguments;
+ if (genericArguments[0] != typeof(string))
+ {
+ return null;
+ }
+
+ elementType = genericArguments[1];
+ var elementMapping = FindPrimitiveMapping(new TypeMappingInfo(elementType));
+ if(elementMapping == null)
+ {
+ return null;
+ }
+
+ return elementMapping == null
+ ? null
+ : new CosmosTypeMapping(clrType,
+ CreateStringDictionaryComparer(elementMapping, elementType, clrType, genericTypeDefinition == typeof(IReadOnlyDictionary<,>)));
+ }
+ }
+
+ return null;
+ }
+
+ private static ValueComparer CreateArrayComparer(CoreTypeMapping elementMapping, Type elementType)
+ {
+ var unwrappedType = elementType.UnwrapNullableType();
+
+ return (ValueComparer)Activator.CreateInstance(
+ elementType == unwrappedType
+ ? typeof(SingleDimensionalArrayComparer<>).MakeGenericType(elementType)
+ : typeof(NullableSingleDimensionalArrayComparer<>).MakeGenericType(unwrappedType),
+ elementMapping.Comparer)!;
+ }
+
+ private static ValueComparer CreateListComparer(
+ CoreTypeMapping elementMapping, Type elementType, Type listType, bool readOnly)
+ {
+ var unwrappedType = elementType.UnwrapNullableType();
+
+ return (ValueComparer)Activator.CreateInstance(
+ elementType == unwrappedType
+ ? typeof(ListComparer<,>).MakeGenericType(elementType, listType)
+ : typeof(NullableListComparer<,>).MakeGenericType(unwrappedType, listType),
+ elementMapping.Comparer,
+ readOnly)!;
+ }
+
+ private static ValueComparer CreateStringDictionaryComparer(
+ CoreTypeMapping elementMapping, Type elementType, Type dictType, bool readOnly)
+ {
+ var unwrappedType = elementType.UnwrapNullableType();
+
+ return (ValueComparer)Activator.CreateInstance(
+ elementType == unwrappedType
+ ? typeof(StringDictionaryComparer<,>).MakeGenericType(elementType, dictType)
+ : typeof(NullableStringDictionaryComparer<,>).MakeGenericType(unwrappedType, dictType),
+ elementMapping.Comparer,
+ readOnly)!;
}
}
}
diff --git a/src/EFCore/Storage/CoreTypeMapping.cs b/src/EFCore/Storage/CoreTypeMapping.cs
index 6aa5cf863dc..5de231426e9 100644
--- a/src/EFCore/Storage/CoreTypeMapping.cs
+++ b/src/EFCore/Storage/CoreTypeMapping.cs
@@ -146,11 +146,21 @@ protected CoreTypeMapping(CoreTypeMappingParameters parameters)
var clrType = converter?.ModelClrType ?? parameters.ClrType;
ClrType = clrType;
+ Check.DebugAssert(parameters.Comparer == null
+ || parameters.ClrType == null
+ || converter != null
+ || parameters.Comparer.Type == parameters.ClrType,
+ $"Expected {parameters.ClrType}, got {parameters.Comparer?.Type}");
if (parameters.Comparer?.Type == clrType)
{
_comparer = parameters.Comparer;
}
+ Check.DebugAssert(parameters.KeyComparer == null
+ || parameters.ClrType == null
+ || converter != null
+ || parameters.KeyComparer.Type == parameters.ClrType,
+ $"Expected {parameters.ClrType}, got {parameters.KeyComparer?.Type}");
if (parameters.KeyComparer?.Type == clrType)
{
_keyComparer = parameters.KeyComparer;
diff --git a/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs
index 99b8c47d794..7a4f38700de 100644
--- a/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs
@@ -2,9 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Azure.Cosmos;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+using Microsoft.EntityFrameworkCore.Cosmos.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Internal;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata.Conventions;
@@ -462,6 +466,172 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
}
}
+ [ConditionalFact]
+ public async Task Can_add_update_delete_with_collections()
+ {
+ await Can_add_update_delete_with_collection(
+ new List { 1, 2 },
+ c =>
+ {
+ c.Collection.Clear();
+ c.Collection.Add(3);
+ },
+ new List { 3 });
+
+ await Can_add_update_delete_with_collection>(
+ new List(),
+ c =>
+ {
+ c.Collection.Clear();
+ c.Collection.Add(3);
+ c.Collection.Add(null);
+ },
+ new List { 3, null });
+
+ await Can_add_update_delete_with_collection>(
+ new[] { "1", null },
+ c =>
+ {
+ c.Collection = new List { "3", "2", "1" };
+ },
+ new List { "3", "2", "1" });
+
+ // See #25343
+ await Can_add_update_delete_with_collection(
+ new List { EntityType.Base, EntityType.Derived, EntityType.Derived },
+ c =>
+ {
+ c.Collection.Clear();
+ c.Collection.Add(EntityType.Base);
+ },
+ new List { EntityType.Base },
+ modelBuilder => modelBuilder.Entity>>(c =>
+ c.Property(s => s.Collection)
+ .HasConversion(m => m.Select(v => (int)v).ToList(), p => p.Select(v => (EntityType)v).ToList(),
+ new ListComparer>(ValueComparer.CreateDefault(typeof(EntityType), false), readOnly: false))));
+
+ await Can_add_update_delete_with_collection(
+ new[] { 1f, 2 },
+ c =>
+ {
+ c.Collection[0] = 3f;
+ },
+ new[] { 3f, 2 });
+
+
+ await Can_add_update_delete_with_collection(
+ new decimal?[] { 1, null },
+ c =>
+ {
+ c.Collection[0] = 3;
+ },
+ new decimal?[] { 3, null });
+
+ await Can_add_update_delete_with_collection(
+ new Dictionary { { "1", 1 } },
+ c =>
+ {
+ c.Collection["2"] = 3;
+ },
+ new Dictionary { { "1", 1 }, { "2", 3 } });
+
+ await Can_add_update_delete_with_collection>(
+ new SortedDictionary { { "2", 2 }, { "1", 1 } },
+ c =>
+ {
+ c.Collection.Clear();
+ c.Collection["2"] = null;
+ },
+ new SortedDictionary { { "2", null } });
+
+ await Can_add_update_delete_with_collection>(
+ ImmutableDictionary.Empty
+ .Add("2", 2).Add("1", 1),
+ c =>
+ {
+ c.Collection = ImmutableDictionary.Empty.Add("1", 1).Add("2", null);
+ },
+ new Dictionary { { "1", 1 }, { "2", null } });
+ }
+
+ private async Task Can_add_update_delete_with_collection(
+ TCollection initialValue,
+ Action> modify,
+ TCollection modifiedValue,
+ Action onModelBuilder = null)
+ where TCollection : class
+ {
+ var options = Fixture.CreateOptions();
+
+ var customer = new CustomerWithCollection { Id = 42, Name = "Theon", Collection = initialValue };
+
+ using (var context = new CollectionCustomerContext(options, onModelBuilder))
+ {
+ await context.Database.EnsureCreatedAsync();
+
+ context.Add(customer);
+
+ await context.SaveChangesAsync();
+ }
+
+ using (var context = new CollectionCustomerContext(options))
+ {
+ var customerFromStore = await context.Customers.SingleAsync();
+
+ Assert.Equal(42, customerFromStore.Id);
+ Assert.Equal(initialValue, customerFromStore.Collection);
+
+ modify(customerFromStore);
+
+ await context.SaveChangesAsync();
+ }
+
+ using (var context = new CollectionCustomerContext(options))
+ {
+ var customerFromStore = await context.Customers.SingleAsync();
+
+ Assert.Equal(42, customerFromStore.Id);
+ Assert.Equal(modifiedValue, customerFromStore.Collection);
+
+ customerFromStore.Collection = null;
+
+ await context.SaveChangesAsync();
+ }
+
+ using (var context = new CollectionCustomerContext(options))
+ {
+ var customerFromStore = await context.Customers.SingleAsync();
+
+ Assert.Equal(42, customerFromStore.Id);
+ Assert.Null(customerFromStore.Collection);
+ }
+ }
+
+ private class CustomerWithCollection
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public TCollection Collection { get; set; }
+ }
+
+ private class CollectionCustomerContext : DbContext
+ {
+ private readonly Action _onModelBuilder;
+
+ public DbSet> Customers { get; set; }
+
+ public CollectionCustomerContext(DbContextOptions dbContextOptions, Action onModelBuilder = null)
+ : base(dbContextOptions)
+ {
+ _onModelBuilder = onModelBuilder;
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ _onModelBuilder?.Invoke(modelBuilder);
+ }
+ }
+
[ConditionalFact]
public async Task Can_read_with_find_with_resource_id_async()
{
diff --git a/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs b/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs
index 6b5850d310e..e19682fec6b 100644
--- a/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs
+++ b/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs
@@ -31,6 +31,20 @@ public override void Properties_specified_by_string_are_shadow_properties_unless
// Fails due to extra shadow properties
}
+ protected override void Mapping_throws_for_non_ignored_array()
+ {
+ var modelBuilder = CreateModelBuilder();
+
+ modelBuilder.Entity();
+
+ var model = modelBuilder.FinalizeModel();
+ var entityType = model.FindEntityType(typeof(OneDee));
+
+ var property = entityType.FindProperty(nameof(OneDee.One));
+ Assert.Null(property.GetProviderClrType());
+ Assert.NotNull(property.FindTypeMapping());
+ }
+
[ConditionalFact]
public override void Properties_can_have_provider_type_set_for_type()
{