diff --git a/src/EFCore.Relational/Storage/RelationalTypeMappingInfo.cs b/src/EFCore.Relational/Storage/RelationalTypeMappingInfo.cs
index af74ab9d33f..bfa35ce92f5 100644
--- a/src/EFCore.Relational/Storage/RelationalTypeMappingInfo.cs
+++ b/src/EFCore.Relational/Storage/RelationalTypeMappingInfo.cs
@@ -251,6 +251,15 @@ public bool IsKeyOrIndex
init => _coreTypeMappingInfo = _coreTypeMappingInfo with { IsKeyOrIndex = value };
}
+ ///
+ /// Indicates whether or not the mapping should be compared, etc. as if it is a key.
+ ///
+ public bool HasKeySemantics
+ {
+ get => _coreTypeMappingInfo.HasKeySemantics;
+ init => _coreTypeMappingInfo = _coreTypeMappingInfo with { HasKeySemantics = value };
+ }
+
///
/// Indicates whether or not the mapping supports Unicode, or if not defined.
///
diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs
index 05a85fdb863..8d0d053cd62 100644
--- a/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs
+++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs
@@ -18,6 +18,8 @@ public class SqlServerStringTypeMapping : StringTypeMapping
private const int UnicodeMax = 4000;
private const int AnsiMax = 8000;
+ private static readonly CaseInsensitiveValueComparer CaseInsensitiveValueComparer = new();
+
private readonly bool _isUtf16;
private readonly SqlDbType? _sqlDbType;
private readonly int _maxSpecificSize;
@@ -35,10 +37,14 @@ public SqlServerStringTypeMapping(
int? size = null,
bool fixedLength = false,
SqlDbType? sqlDbType = null,
- StoreTypePostfix? storeTypePostfix = null)
+ StoreTypePostfix? storeTypePostfix = null,
+ bool useKeyComparison = false)
: this(
new RelationalTypeMappingParameters(
- new CoreTypeMappingParameters(typeof(string)),
+ new CoreTypeMappingParameters(
+ typeof(string),
+ comparer: useKeyComparison ? CaseInsensitiveValueComparer : null,
+ keyComparer: useKeyComparison ? CaseInsensitiveValueComparer : null),
storeType ?? GetDefaultStoreName(unicode, fixedLength),
storeTypePostfix ?? StoreTypePostfix.Size,
GetDbType(unicode, fixedLength),
diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs
index b99ddb0a384..16c33c86b68 100644
--- a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs
+++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs
@@ -336,7 +336,8 @@ public SqlServerTypeMappingSource(
}
if (size == null
- && storeTypeName == null)
+ && storeTypeName == null
+ && !mappingInfo.HasKeySemantics)
{
return isAnsi
? isFixedLength
@@ -351,7 +352,8 @@ public SqlServerTypeMappingSource(
unicode: !isAnsi,
size: size,
fixedLength: isFixedLength,
- storeTypePostfix: storeTypeName == null ? StoreTypePostfix.Size : StoreTypePostfix.None);
+ storeTypePostfix: storeTypeName == null ? StoreTypePostfix.Size : StoreTypePostfix.None,
+ useKeyComparison: mappingInfo.HasKeySemantics);
}
if (clrType == typeof(byte[]))
diff --git a/src/EFCore/ChangeTracking/CaseInsensitiveValueComparer.cs b/src/EFCore/ChangeTracking/CaseInsensitiveValueComparer.cs
new file mode 100644
index 00000000000..41ec1c7000a
--- /dev/null
+++ b/src/EFCore/ChangeTracking/CaseInsensitiveValueComparer.cs
@@ -0,0 +1,20 @@
+// 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;
+
+///
+/// Case-insensitive value comparison for strings.
+///
+public class CaseInsensitiveValueComparer : ValueComparer
+{
+ ///
+ /// Creates a value comparer instance.
+ ///
+ public CaseInsensitiveValueComparer()
+ : base(
+ (l, r) => string.Equals(l, r, StringComparison.OrdinalIgnoreCase),
+ v => v == null ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(v))
+ {
+ }
+}
diff --git a/src/EFCore/Storage/TypeMappingInfo.cs b/src/EFCore/Storage/TypeMappingInfo.cs
index c67514ccc9e..310247bd76e 100644
--- a/src/EFCore/Storage/TypeMappingInfo.cs
+++ b/src/EFCore/Storage/TypeMappingInfo.cs
@@ -97,7 +97,8 @@ public TypeMappingInfo(
var mappingHints = customConverter?.MappingHints;
var property = principals[0];
- IsKeyOrIndex = property.IsKey() || property.IsForeignKey() || property.IsIndex();
+ HasKeySemantics = property.IsKey() || property.IsForeignKey();
+ IsKeyOrIndex = HasKeySemantics || property.IsIndex();
Size = fallbackSize ?? mappingHints?.Size;
IsUnicode = fallbackUnicode ?? mappingHints?.IsUnicode;
IsRowVersion = property.IsConcurrencyToken && property.ValueGenerated == ValueGenerated.OnAddOrUpdate;
@@ -138,6 +139,7 @@ public TypeMappingInfo(
/// Specifies a row-version, or for default.
/// Specifies a precision for the mapping, or for default.
/// Specifies a scale for the mapping, or for default.
+ /// If , then a special mapping for a key or foreign key may be returned.
public TypeMappingInfo(
Type? type = null,
bool keyOrIndex = false,
@@ -145,11 +147,13 @@ public TypeMappingInfo(
int? size = null,
bool? rowVersion = null,
int? precision = null,
- int? scale = null)
+ int? scale = null,
+ bool keySemantics = false)
{
ClrType = type?.UnwrapNullableType();
IsKeyOrIndex = keyOrIndex;
+ HasKeySemantics = keySemantics;
Size = size;
IsUnicode = unicode;
IsRowVersion = rowVersion;
@@ -176,6 +180,7 @@ public TypeMappingInfo(
{
IsRowVersion = source.IsRowVersion;
IsKeyOrIndex = source.IsKeyOrIndex;
+ HasKeySemantics = source.HasKeySemantics;
var mappingHints = converter.MappingHints;
@@ -200,6 +205,11 @@ public TypeMappingInfo WithConverter(in ValueConverterInfo converterInfo)
///
public bool IsKeyOrIndex { get; init; }
+ ///
+ /// Indicates whether or not the mapping should be compared, etc. as if it is a key.
+ ///
+ public bool HasKeySemantics { get; init; }
+
///
/// Indicates the store-size to use for the mapping, or null if none.
///
diff --git a/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs b/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs
index 1ec8dfb4bad..773db21b4af 100644
--- a/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs
+++ b/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs
@@ -1141,18 +1141,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
v => v.Skip(3).ToArray());
});
- var caseInsensitiveComparer = new ValueComparer(
- (l, r) => (l == null || r == null) ? (l == r) : l.Equals(r, StringComparison.InvariantCultureIgnoreCase),
- v => StringComparer.InvariantCultureIgnoreCase.GetHashCode(v),
- v => v);
-
modelBuilder.Entity(
b =>
{
var property = b.Property(e => e.Id)
.HasConversion(v => "KeyValue=" + v, v => v.Substring(9)).Metadata;
-
- property.SetValueComparer(caseInsensitiveComparer);
});
modelBuilder.Entity(
@@ -1161,8 +1154,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
b.Property(e => e.StringKeyDataTypeId)
.HasConversion(
v => "KeyValue=" + v,
- v => v.Substring(9),
- caseInsensitiveComparer);
+ v => v.Substring(9));
});
modelBuilder.Entity(