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(