Skip to content
25 changes: 24 additions & 1 deletion src/EFCore/ChangeTracking/ValueComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -304,10 +304,14 @@ public static ValueComparer CreateDefault
return new DefaultDateTimeOffsetValueComparer(favorStructuralComparisons);
}

if (nonNullableType == typeof(string))
{
return new DefaultStringValueComparer(favorStructuralComparisons);
}
Comment on lines +307 to +310

return nonNullableType.IsInteger()
|| nonNullableType == typeof(decimal)
|| nonNullableType == typeof(bool)
|| nonNullableType == typeof(string)
|| nonNullableType == typeof(DateTime)
|| nonNullableType == typeof(DateOnly)
|| nonNullableType == typeof(Guid)
Expand Down Expand Up @@ -376,4 +380,23 @@ private static readonly MethodInfo EqualsExactMethodInfo
public override Expression ExtractEqualsBody(Expression leftExpression, Expression rightExpression)
=> Expression.Call(leftExpression, EqualsExactMethodInfo, rightExpression);
}

#pragma warning disable CA1309 // Use ordinal StringComparison - InvariantCulture is intentional to handle Unicode canonical equivalence (NFC/NFD)
internal sealed class DefaultStringValueComparer(bool favorStructuralComparisons)
: DefaultValueComparer<string>((v1, v2) => string.Equals(v1, v2, StringComparison.InvariantCulture), favorStructuralComparisons)
{
private static readonly MethodInfo StringEqualsMethodInfo
= typeof(string).GetMethod(nameof(string.Equals), [typeof(string), typeof(string), typeof(StringComparison)])!;

// String canonical equivalence (e.g. NFC vs NFD Unicode normalization) is handled by InvariantCulture comparison.
// Override hash code to be consistent with InvariantCulture equality so that canonically equivalent strings
// produce the same hash code.
public override int GetHashCode(string instance)
=> StringComparer.InvariantCulture.GetHashCode(instance);

public override Expression ExtractEqualsBody(Expression leftExpression, Expression rightExpression)
=> Expression.Call(StringEqualsMethodInfo, leftExpression, rightExpression, Expression.Constant(StringComparison.InvariantCulture));
}
Comment on lines +384 to +399
#pragma warning restore CA1309
}

17 changes: 15 additions & 2 deletions src/EFCore/ChangeTracking/ValueComparerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ public static class ValueComparerExtensions
/// <param name="valueComparer">The value comparer.</param>
/// <returns><see langword="true" /> if the value comparer is the default; <see langword="false" /> otherwise.</returns>
public static bool IsDefault(this ValueComparer valueComparer)
=> valueComparer.GetType().IsGenericType
&& valueComparer.GetType().GetGenericTypeDefinition() == typeof(ValueComparer.DefaultValueComparer<>);
{
var type = valueComparer.GetType();
while (type != null && type != typeof(object))
{
if (type.IsGenericType
&& type.GetGenericTypeDefinition() == typeof(ValueComparer.DefaultValueComparer<>))
{
return true;
}

type = type.BaseType;
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
int (long v) => ((object)v).GetHashCode(),
long (long v) => v),
providerValueComparer: new ValueComparer<string>(
bool (string v1, string v2) => v1 == v2,
bool (string v1, string v2) => string.Equals(v1, v2, StringComparison.InvariantCulture),
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
converter: new ValueConverter<long, string>(
Expand Down Expand Up @@ -151,7 +151,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
int (byte[] v) => StructuralComparisons.StructuralEqualityComparer.GetHashCode(((object)v)),
byte[] (byte[] source) => source.ToArray()),
providerValueComparer: new ValueComparer<string>(
bool (string v1, string v2) => v1 == v2,
bool (string v1, string v2) => string.Equals(v1, v2, StringComparison.InvariantCulture),
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
converter: new ValueConverter<byte[], string>(
Expand Down Expand Up @@ -271,7 +271,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
storeGenerationIndex: -1);
map.TypeMapping = CosmosTypeMapping.Default.Clone(
comparer: new StringDictionaryComparer<Dictionary<string, string[]>, string[]>(new ConvertingValueComparer<string[], object>(new ListOfReferenceTypesComparer<string[], string>(new ValueComparer<string>(
bool (string v1, string v2) => v1 == v2,
bool (string v1, string v2) => string.Equals(v1, v2, StringComparison.InvariantCulture),
int (string v) => ((object)v).GetHashCode(),
string (string v) => v)))),
keyComparer: new ValueComparer<Dictionary<string, string[]>>(
Expand Down Expand Up @@ -305,15 +305,15 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
storeGenerationIndex: -1);
__id.TypeMapping = CosmosTypeMapping.Default.Clone(
comparer: new ValueComparer<string>(
bool (string v1, string v2) => v1 == v2,
bool (string v1, string v2) => string.Equals(v1, v2, StringComparison.InvariantCulture),
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
keyComparer: new ValueComparer<string>(
bool (string v1, string v2) => v1 == v2,
bool (string v1, string v2) => string.Equals(v1, v2, StringComparison.InvariantCulture),
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
providerValueComparer: new ValueComparer<string>(
bool (string v1, string v2) => v1 == v2,
bool (string v1, string v2) => string.Equals(v1, v2, StringComparison.InvariantCulture),
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
clrType: typeof(string),
Expand Down Expand Up @@ -375,15 +375,15 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
storeGenerationIndex: 1);
_etag.TypeMapping = CosmosTypeMapping.Default.Clone(
comparer: new ValueComparer<string>(
bool (string v1, string v2) => v1 == v2,
bool (string v1, string v2) => string.Equals(v1, v2, StringComparison.InvariantCulture),
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
keyComparer: new ValueComparer<string>(
bool (string v1, string v2) => v1 == v2,
bool (string v1, string v2) => string.Equals(v1, v2, StringComparison.InvariantCulture),
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
providerValueComparer: new ValueComparer<string>(
bool (string v1, string v2) => v1 == v2,
bool (string v1, string v2) => string.Equals(v1, v2, StringComparison.InvariantCulture),
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
clrType: typeof(string),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
int (Guid v) => ((object)v).GetHashCode(),
Guid (Guid v) => v),
providerValueComparer: new ValueComparer<string>(
bool (string v1, string v2) => v1 == v2,
bool (string v1, string v2) => string.Equals(v1, v2, StringComparison.InvariantCulture),
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
converter: new ValueConverter<Guid, string>(
Expand Down Expand Up @@ -217,15 +217,15 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
storeGenerationIndex: -1);
__id.TypeMapping = CosmosTypeMapping.Default.Clone(
comparer: new ValueComparer<string>(
bool (string v1, string v2) => v1 == v2,
bool (string v1, string v2) => string.Equals(v1, v2, StringComparison.InvariantCulture),
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
keyComparer: new ValueComparer<string>(
bool (string v1, string v2) => v1 == v2,
bool (string v1, string v2) => string.Equals(v1, v2, StringComparison.InvariantCulture),
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
providerValueComparer: new ValueComparer<string>(
bool (string v1, string v2) => v1 == v2,
bool (string v1, string v2) => string.Equals(v1, v2, StringComparison.InvariantCulture),
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
clrType: typeof(string),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,15 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
storeGenerationIndex: -1);
data.TypeMapping = CosmosTypeMapping.Default.Clone(
comparer: new ValueComparer<string>(
bool (string v1, string v2) => v1 == v2,
bool (string v1, string v2) => string.Equals(v1, v2, StringComparison.InvariantCulture),
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
keyComparer: new ValueComparer<string>(
bool (string v1, string v2) => v1 == v2,
bool (string v1, string v2) => string.Equals(v1, v2, StringComparison.InvariantCulture),
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
providerValueComparer: new ValueComparer<string>(
bool (string v1, string v2) => v1 == v2,
bool (string v1, string v2) => string.Equals(v1, v2, StringComparison.InvariantCulture),
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
clrType: typeof(string),
Expand Down
Loading
Loading