From 10fd0eae0e61eed7347a85d92c6b3ffc32a8909c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 05:26:03 +0000 Subject: [PATCH 1/2] Initial plan From b5d6caa23a86d6a160a297d60b85b5f74a3bc2c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 05:49:19 +0000 Subject: [PATCH 2/2] Fix invalid geography V flag by always setting IsValid to true in binary output SqlServerBytesWriter v2.1.0 sets the IsValid flag in the SQL Server binary format based on NTS's Geometry.IsValid, which uses different validation rules than SQL Server. SQL Server's STIsValid() reads the V flag from the binary format without re-validating. This causes geometries that are invalid in NTS (but potentially valid in SQL Server) to be incorrectly marked as invalid. The fix ensures the IsValid flag in the binary output is always set to true, preventing NTS validation rules from affecting SQL Server validity. Fixes #37416 Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Internal/GeometryValueConverter.cs | 20 +++- .../Internal/GeometryValueConverterTest.cs | 108 ++++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 test/EFCore.SqlServer.Tests/Storage/ValueConversion/Internal/GeometryValueConverterTest.cs diff --git a/src/EFCore.SqlServer.NTS/Storage/ValueConversion/Internal/GeometryValueConverter.cs b/src/EFCore.SqlServer.NTS/Storage/ValueConversion/Internal/GeometryValueConverter.cs index ad187349e99..76d6f415b8b 100644 --- a/src/EFCore.SqlServer.NTS/Storage/ValueConversion/Internal/GeometryValueConverter.cs +++ b/src/EFCore.SqlServer.NTS/Storage/ValueConversion/Internal/GeometryValueConverter.cs @@ -16,6 +16,14 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.ValueConversion.Intern public class GeometryValueConverter : ValueConverter where TGeometry : Geometry { + // The IsValid flag is at bit 2 (0x04) of the Properties byte at offset 5 in the SQL Server + // geography/geometry binary format (MS-SSCLRT). SqlServerBytesWriter sets this flag based on + // NTS's Geometry.IsValid, but NTS and SQL Server use different validation rules. We always set + // this flag to true to avoid NTS validation rules incorrectly marking geometries as invalid + // in SQL Server. See https://github.com/dotnet/efcore/issues/37416 + private const int PropertiesByteIndex = 5; + private const byte IsValidFlag = 0x04; + /// /// 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 @@ -24,8 +32,18 @@ public class GeometryValueConverter : ValueConverter public GeometryValueConverter(SqlServerBytesReader reader, SqlServerBytesWriter writer) : base( - g => new SqlBytes(writer.Write(g)), + g => new SqlBytes(SetIsValidFlag(writer.Write(g))), b => (TGeometry)reader.Read(b.Value)) { } + + private static byte[] SetIsValidFlag(byte[] bytes) + { + if (bytes.Length > PropertiesByteIndex) + { + bytes[PropertiesByteIndex] |= IsValidFlag; + } + + return bytes; + } } diff --git a/test/EFCore.SqlServer.Tests/Storage/ValueConversion/Internal/GeometryValueConverterTest.cs b/test/EFCore.SqlServer.Tests/Storage/ValueConversion/Internal/GeometryValueConverterTest.cs new file mode 100644 index 00000000000..0332bd2a5ed --- /dev/null +++ b/test/EFCore.SqlServer.Tests/Storage/ValueConversion/Internal/GeometryValueConverterTest.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Data.SqlTypes; +using NetTopologySuite; +using NetTopologySuite.Geometries; +using NetTopologySuite.IO; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.ValueConversion.Internal; + +public class GeometryValueConverterTest +{ + // The IsValid flag is at bit 2 (0x04) of the Properties byte at offset 5 in the SQL Server + // geography/geometry binary format (MS-SSCLRT). + private const int PropertiesByteIndex = 5; + private const byte IsValidFlag = 0x04; + + [ConditionalFact] + public void IsValid_flag_set_for_valid_geometry() + { + var point = new Point(1, 2) { SRID = 4326 }; + Assert.True(point.IsValid); + + var converter = CreateConverter(isGeography: false); + var sqlBytes = (SqlBytes)converter.ConvertToProvider(point)!; + + Assert.True((sqlBytes.Value[PropertiesByteIndex] & IsValidFlag) != 0, "IsValid flag should be set for valid geometry"); + } + + [ConditionalFact] + public void IsValid_flag_set_for_valid_geography() + { + var point = new Point(1, 2) { SRID = 4326 }; + Assert.True(point.IsValid); + + var converter = CreateConverter(isGeography: true); + var sqlBytes = (SqlBytes)converter.ConvertToProvider(point)!; + + Assert.True((sqlBytes.Value[PropertiesByteIndex] & IsValidFlag) != 0, "IsValid flag should be set for valid geography"); + } + + [ConditionalFact] + public void IsValid_flag_set_for_invalid_geometry() + { + // Create an invalid geometry (self-intersecting polygon - bowtie shape) + var polygon = new Polygon( + new LinearRing( + [ + new Coordinate(0, 0), + new Coordinate(2, 2), + new Coordinate(2, 0), + new Coordinate(0, 2), + new Coordinate(0, 0) + ])); + Assert.False(polygon.IsValid); + + var converter = CreateConverter(isGeography: false); + var sqlBytes = (SqlBytes)converter.ConvertToProvider(polygon)!; + + Assert.True( + (sqlBytes.Value[PropertiesByteIndex] & IsValidFlag) != 0, + "IsValid flag should be set even for NTS-invalid geometry"); + } + + [ConditionalFact] + public void IsValid_flag_set_for_invalid_geography() + { + // Create an invalid geography (self-intersecting polygon - bowtie shape) + var polygon = new Polygon( + new LinearRing( + [ + new Coordinate(0, 0), + new Coordinate(2, 2), + new Coordinate(2, 0), + new Coordinate(0, 2), + new Coordinate(0, 0) + ])) { SRID = 4326 }; + Assert.False(polygon.IsValid); + + var converter = CreateConverter(isGeography: true); + var sqlBytes = (SqlBytes)converter.ConvertToProvider(polygon)!; + + Assert.True( + (sqlBytes.Value[PropertiesByteIndex] & IsValidFlag) != 0, + "IsValid flag should be set even for NTS-invalid geography"); + } + + [ConditionalFact] + public void Roundtrip_preserves_geometry_data() + { + var point = new Point(1, 2) { SRID = 4326 }; + + var converter = CreateConverter(isGeography: false); + var sqlBytes = (SqlBytes)converter.ConvertToProvider(point)!; + var roundtripped = (Point)converter.ConvertFromProvider(sqlBytes)!; + + Assert.Equal(point.X, roundtripped.X); + Assert.Equal(point.Y, roundtripped.Y); + Assert.Equal(point.SRID, roundtripped.SRID); + } + + private static GeometryValueConverter CreateConverter(bool isGeography) + { + var reader = new SqlServerBytesReader(NtsGeometryServices.Instance) { IsGeography = isGeography }; + var writer = new SqlServerBytesWriter { IsGeography = isGeography }; + return new GeometryValueConverter(reader, writer); + } +}