Skip to content

Commit a253360

Browse files
committed
Use case-insensitive string comparisons by default on SQL Server
Fixes #27526
1 parent 38f69c6 commit a253360

File tree

6 files changed

+54
-15
lines changed

6 files changed

+54
-15
lines changed

src/EFCore.Relational/Storage/RelationalTypeMappingInfo.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,15 @@ public bool IsKeyOrIndex
217217
init => _coreTypeMappingInfo = _coreTypeMappingInfo with { IsKeyOrIndex = value };
218218
}
219219

220+
/// <summary>
221+
/// Indicates whether or not the mapping should be compared, etc. as if it is a key.
222+
/// </summary>
223+
public bool HasKeySemantics
224+
{
225+
get => _coreTypeMappingInfo.HasKeySemantics;
226+
init => _coreTypeMappingInfo = _coreTypeMappingInfo with { HasKeySemantics = value };
227+
}
228+
220229
/// <summary>
221230
/// Indicates whether or not the mapping supports Unicode, or <see langword="null" /> if not defined.
222231
/// </summary>

src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public class SqlServerStringTypeMapping : StringTypeMapping
1818
private const int UnicodeMax = 4000;
1919
private const int AnsiMax = 8000;
2020

21+
private static readonly CaseInsensitiveValueComparer CaseInsensitiveValueComparer = new();
22+
2123
private readonly bool _isUtf16;
2224
private readonly SqlDbType? _sqlDbType;
2325
private readonly int _maxSpecificSize;
@@ -35,10 +37,14 @@ public SqlServerStringTypeMapping(
3537
int? size = null,
3638
bool fixedLength = false,
3739
SqlDbType? sqlDbType = null,
38-
StoreTypePostfix? storeTypePostfix = null)
40+
StoreTypePostfix? storeTypePostfix = null,
41+
bool useKeyComparison = false)
3942
: this(
4043
new RelationalTypeMappingParameters(
41-
new CoreTypeMappingParameters(typeof(string)),
44+
new CoreTypeMappingParameters(
45+
typeof(string),
46+
comparer: useKeyComparison ? CaseInsensitiveValueComparer : null,
47+
keyComparer: useKeyComparison ? CaseInsensitiveValueComparer : null),
4248
storeType ?? GetDefaultStoreName(unicode, fixedLength),
4349
storeTypePostfix ?? StoreTypePostfix.Size,
4450
GetDbType(unicode, fixedLength),

src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,8 @@ public SqlServerTypeMappingSource(
306306
}
307307

308308
if (size == null
309-
&& storeTypeName == null)
309+
&& storeTypeName == null
310+
&& !mappingInfo.HasKeySemantics)
310311
{
311312
return isAnsi
312313
? isFixedLength
@@ -321,7 +322,8 @@ public SqlServerTypeMappingSource(
321322
unicode: !isAnsi,
322323
size: size,
323324
fixedLength: isFixedLength,
324-
storeTypePostfix: storeTypeName == null ? StoreTypePostfix.Size : StoreTypePostfix.None);
325+
storeTypePostfix: storeTypeName == null ? StoreTypePostfix.Size : StoreTypePostfix.None,
326+
useKeyComparison: mappingInfo.HasKeySemantics);
325327
}
326328

327329
if (clrType == typeof(byte[]))
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.EntityFrameworkCore.ChangeTracking;
5+
6+
/// <summary>
7+
/// Case-insensitive value comparison for strings.
8+
/// </summary>
9+
public class CaseInsensitiveValueComparer : ValueComparer<string?>
10+
{
11+
/// <summary>
12+
/// Creates a value comparer instance.
13+
/// </summary>
14+
public CaseInsensitiveValueComparer()
15+
: base(
16+
(l, r) => string.Equals(l, r, StringComparison.OrdinalIgnoreCase),
17+
v => v == null ? 0 : v.ToUpper().GetHashCode())
18+
{
19+
}
20+
}

src/EFCore/Storage/TypeMappingInfo.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ public TypeMappingInfo(
9797
var mappingHints = customConverter?.MappingHints;
9898
var property = principals[0];
9999

100-
IsKeyOrIndex = property.IsKey() || property.IsForeignKey() || property.IsIndex();
100+
HasKeySemantics = property.IsKey() || property.IsForeignKey();
101+
IsKeyOrIndex = HasKeySemantics || property.IsIndex();
101102
Size = fallbackSize ?? mappingHints?.Size;
102103
IsUnicode = fallbackUnicode ?? mappingHints?.IsUnicode;
103104
IsRowVersion = property.IsConcurrencyToken && property.ValueGenerated == ValueGenerated.OnAddOrUpdate;
@@ -138,18 +139,21 @@ public TypeMappingInfo(
138139
/// <param name="rowVersion">Specifies a row-version, or <see langword="null" /> for default.</param>
139140
/// <param name="precision">Specifies a precision for the mapping, or <see langword="null" /> for default.</param>
140141
/// <param name="scale">Specifies a scale for the mapping, or <see langword="null" /> for default.</param>
142+
/// <param name="keySemantics">If <see langword="true" />, then a special mapping for a key or foreign key may be returned.</param>
141143
public TypeMappingInfo(
142144
Type? type = null,
143145
bool keyOrIndex = false,
144146
bool? unicode = null,
145147
int? size = null,
146148
bool? rowVersion = null,
147149
int? precision = null,
148-
int? scale = null)
150+
int? scale = null,
151+
bool keySemantics = false)
149152
{
150153
ClrType = type?.UnwrapNullableType();
151154

152155
IsKeyOrIndex = keyOrIndex;
156+
HasKeySemantics = keySemantics;
153157
Size = size;
154158
IsUnicode = unicode;
155159
IsRowVersion = rowVersion;
@@ -176,6 +180,7 @@ public TypeMappingInfo(
176180
{
177181
IsRowVersion = source.IsRowVersion;
178182
IsKeyOrIndex = source.IsKeyOrIndex;
183+
HasKeySemantics = source.HasKeySemantics;
179184

180185
var mappingHints = converter.MappingHints;
181186

@@ -200,6 +205,11 @@ public TypeMappingInfo WithConverter(in ValueConverterInfo converterInfo)
200205
/// </summary>
201206
public bool IsKeyOrIndex { get; init; }
202207

208+
/// <summary>
209+
/// Indicates whether or not the mapping should be compared, etc. as if it is a key.
210+
/// </summary>
211+
public bool HasKeySemantics { get; init; }
212+
203213
/// <summary>
204214
/// Indicates the store-size to use for the mapping, or null if none.
205215
/// </summary>

test/EFCore.Specification.Tests/CustomConvertersTestBase.cs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,18 +1141,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
11411141
v => v.Skip(3).ToArray());
11421142
});
11431143

1144-
var caseInsensitiveComparer = new ValueComparer<string>(
1145-
(l, r) => (l == null || r == null) ? (l == r) : l.Equals(r, StringComparison.InvariantCultureIgnoreCase),
1146-
v => StringComparer.InvariantCultureIgnoreCase.GetHashCode(v),
1147-
v => v);
1148-
11491144
modelBuilder.Entity<StringKeyDataType>(
11501145
b =>
11511146
{
11521147
var property = b.Property(e => e.Id)
11531148
.HasConversion(v => "KeyValue=" + v, v => v.Substring(9)).Metadata;
1154-
1155-
property.SetValueComparer(caseInsensitiveComparer);
11561149
});
11571150

11581151
modelBuilder.Entity<StringForeignKeyDataType>(
@@ -1161,8 +1154,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
11611154
b.Property(e => e.StringKeyDataTypeId)
11621155
.HasConversion(
11631156
v => "KeyValue=" + v,
1164-
v => v.Substring(9),
1165-
caseInsensitiveComparer);
1157+
v => v.Substring(9));
11661158
});
11671159

11681160
modelBuilder.Entity<MaxLengthDataTypes>(

0 commit comments

Comments
 (0)