Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.ValueConversion.Intern
public class GeometryValueConverter<TGeometry> : ValueConverter<TGeometry, SqlBytes>
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;

/// <summary>
/// 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
Expand All @@ -24,8 +32,18 @@ public class GeometryValueConverter<TGeometry> : ValueConverter<TGeometry, SqlBy
/// </summary>
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;
}
}
Original file line number Diff line number Diff line change
@@ -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<Geometry> CreateConverter(bool isGeography)
{
var reader = new SqlServerBytesReader(NtsGeometryServices.Instance) { IsGeography = isGeography };
var writer = new SqlServerBytesWriter { IsGeography = isGeography };
return new GeometryValueConverter<Geometry>(reader, writer);
}
}