diff --git a/AspNetCoreOData.NetTopologySuite.sln b/AspNetCoreOData.NetTopologySuite.sln
new file mode 100644
index 000000000..fb906eb27
--- /dev/null
+++ b/AspNetCoreOData.NetTopologySuite.sln
@@ -0,0 +1,71 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.OData", "src\Microsoft.AspNetCore.OData\Microsoft.AspNetCore.OData.csproj", "{79D9A62B-36EB-4345-AFCE-081397C29F7E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.OData.NetTopologySuite", "src\Microsoft.AspNetCore.OData.NetTopologySuite\Microsoft.AspNetCore.OData.NetTopologySuite.csproj", "{57ECAA5E-54AB-46AD-A7C6-981BF39E3620}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0C88DD14-F956-CE84-757C-A364CCF449FC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.OData.NetTopologySuite.Tests", "test\Microsoft.AspNetCore.OData.NetTopologySuite.Tests\Microsoft.AspNetCore.OData.NetTopologySuite.Tests.csproj", "{5E8F75EF-B325-4564-8EAF-A39BAC8FE4D8}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {79D9A62B-36EB-4345-AFCE-081397C29F7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {79D9A62B-36EB-4345-AFCE-081397C29F7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {79D9A62B-36EB-4345-AFCE-081397C29F7E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {79D9A62B-36EB-4345-AFCE-081397C29F7E}.Debug|x64.Build.0 = Debug|Any CPU
+ {79D9A62B-36EB-4345-AFCE-081397C29F7E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {79D9A62B-36EB-4345-AFCE-081397C29F7E}.Debug|x86.Build.0 = Debug|Any CPU
+ {79D9A62B-36EB-4345-AFCE-081397C29F7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {79D9A62B-36EB-4345-AFCE-081397C29F7E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {79D9A62B-36EB-4345-AFCE-081397C29F7E}.Release|x64.ActiveCfg = Release|Any CPU
+ {79D9A62B-36EB-4345-AFCE-081397C29F7E}.Release|x64.Build.0 = Release|Any CPU
+ {79D9A62B-36EB-4345-AFCE-081397C29F7E}.Release|x86.ActiveCfg = Release|Any CPU
+ {79D9A62B-36EB-4345-AFCE-081397C29F7E}.Release|x86.Build.0 = Release|Any CPU
+ {57ECAA5E-54AB-46AD-A7C6-981BF39E3620}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {57ECAA5E-54AB-46AD-A7C6-981BF39E3620}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {57ECAA5E-54AB-46AD-A7C6-981BF39E3620}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {57ECAA5E-54AB-46AD-A7C6-981BF39E3620}.Debug|x64.Build.0 = Debug|Any CPU
+ {57ECAA5E-54AB-46AD-A7C6-981BF39E3620}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {57ECAA5E-54AB-46AD-A7C6-981BF39E3620}.Debug|x86.Build.0 = Debug|Any CPU
+ {57ECAA5E-54AB-46AD-A7C6-981BF39E3620}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {57ECAA5E-54AB-46AD-A7C6-981BF39E3620}.Release|Any CPU.Build.0 = Release|Any CPU
+ {57ECAA5E-54AB-46AD-A7C6-981BF39E3620}.Release|x64.ActiveCfg = Release|Any CPU
+ {57ECAA5E-54AB-46AD-A7C6-981BF39E3620}.Release|x64.Build.0 = Release|Any CPU
+ {57ECAA5E-54AB-46AD-A7C6-981BF39E3620}.Release|x86.ActiveCfg = Release|Any CPU
+ {57ECAA5E-54AB-46AD-A7C6-981BF39E3620}.Release|x86.Build.0 = Release|Any CPU
+ {5E8F75EF-B325-4564-8EAF-A39BAC8FE4D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5E8F75EF-B325-4564-8EAF-A39BAC8FE4D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5E8F75EF-B325-4564-8EAF-A39BAC8FE4D8}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5E8F75EF-B325-4564-8EAF-A39BAC8FE4D8}.Debug|x64.Build.0 = Debug|Any CPU
+ {5E8F75EF-B325-4564-8EAF-A39BAC8FE4D8}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5E8F75EF-B325-4564-8EAF-A39BAC8FE4D8}.Debug|x86.Build.0 = Debug|Any CPU
+ {5E8F75EF-B325-4564-8EAF-A39BAC8FE4D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5E8F75EF-B325-4564-8EAF-A39BAC8FE4D8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5E8F75EF-B325-4564-8EAF-A39BAC8FE4D8}.Release|x64.ActiveCfg = Release|Any CPU
+ {5E8F75EF-B325-4564-8EAF-A39BAC8FE4D8}.Release|x64.Build.0 = Release|Any CPU
+ {5E8F75EF-B325-4564-8EAF-A39BAC8FE4D8}.Release|x86.ActiveCfg = Release|Any CPU
+ {5E8F75EF-B325-4564-8EAF-A39BAC8FE4D8}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {79D9A62B-36EB-4345-AFCE-081397C29F7E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+ {57ECAA5E-54AB-46AD-A7C6-981BF39E3620} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+ {5E8F75EF-B325-4564-8EAF-A39BAC8FE4D8} = {0C88DD14-F956-CE84-757C-A364CCF449FC}
+ EndGlobalSection
+EndGlobal
diff --git a/src/Microsoft.AspNetCore.OData.NetTopologySuite.Nightly.nuspec b/src/Microsoft.AspNetCore.OData.NetTopologySuite.Nightly.nuspec
new file mode 100644
index 000000000..dfe6fdea1
--- /dev/null
+++ b/src/Microsoft.AspNetCore.OData.NetTopologySuite.Nightly.nuspec
@@ -0,0 +1,38 @@
+
+
+
+ Microsoft.AspNetCore.OData.NetTopologySuite
+ ASP.NET Core OData NetTopologySuite Integration
+ $VersionFullSemantic$-Nightly$NightlyBuildVersion$
+ Microsoft
+ © Microsoft Corporation. All rights reserved.
+ Adds NetTopologySuite (NTS) spatial support to ASP.NET Core OData. Enables using NTS geometry types (Point, LineString, Polygon, Multi*, GeometryCollection) as Edm.Geometry, and opt-in Edm.Geography mapping via the [Geography] attribute and conventions. Includes an EDM type-mapping provider and spatial serializers/deserializers.
+ NetTopologySuite spatial support for ASP.NET Core OData.
+ en-US
+ http://github.com/OData/AspNetCoreOData
+ MIT
+ true
+ Microsoft AspNetCore WebApi OData
+ docs\README.md
+ images\odata.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.OData.NetTopologySuite.Release.nuspec b/src/Microsoft.AspNetCore.OData.NetTopologySuite.Release.nuspec
new file mode 100644
index 000000000..10ffc3fac
--- /dev/null
+++ b/src/Microsoft.AspNetCore.OData.NetTopologySuite.Release.nuspec
@@ -0,0 +1,38 @@
+
+
+
+ Microsoft.AspNetCore.OData.NetTopologySuite
+ ASP.NET Core OData NetTopologySuite Integration
+ $VersionNuGetSemantic$
+ Microsoft
+ © Microsoft Corporation. All rights reserved.
+ Adds NetTopologySuite (NTS) spatial support to ASP.NET Core OData. Enables using NTS geometry types (Point, LineString, Polygon, Multi*, GeometryCollection) as Edm.Geometry, and opt-in Edm.Geography mapping via the [Geography] attribute and conventions. Includes an EDM type-mapping provider and spatial serializers/deserializers.
+ NetTopologySuite spatial support for ASP.NET Core OData.
+ en-US
+ http://github.com/OData/AspNetCoreOData
+ MIT
+ true
+ Microsoft AspNetCore OData NetTopologySuite
+ docs\README.md
+ images\odata.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.OData.NetTopologySuite/Attributes/GeographyAttribute.cs b/src/Microsoft.AspNetCore.OData.NetTopologySuite/Attributes/GeographyAttribute.cs
new file mode 100644
index 000000000..e6da4a45b
--- /dev/null
+++ b/src/Microsoft.AspNetCore.OData.NetTopologySuite/Attributes/GeographyAttribute.cs
@@ -0,0 +1,30 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using Microsoft.AspNetCore.OData.NetTopologySuite.Conventions;
+using Microsoft.OData.Edm;
+using Geometry = NetTopologySuite.Geometries.Geometry;
+
+namespace Microsoft.AspNetCore.OData.NetTopologySuite.Attributes;
+
+///
+/// Marks a property or CLR type so that NetTopologySuite geometry values are modeled as Edm geography types.
+///
+///
+/// - When applied to a property whose CLR type derives from ,
+/// the property is mapped to the corresponding in the Edm.Geography* family
+/// (for example, Point → GeographyPoint).
+/// - When applied to a class, all properties on the type whose CLR types derive from
+/// are treated as Edm.Geography*.
+/// The behavior is enforced by the conventions
+/// and
+/// .
+///
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
+public class GeographyAttribute : Attribute
+{
+}
diff --git a/src/Microsoft.AspNetCore.OData.NetTopologySuite/Common/Error.cs b/src/Microsoft.AspNetCore.OData.NetTopologySuite/Common/Error.cs
new file mode 100644
index 000000000..bcbeb4f1a
--- /dev/null
+++ b/src/Microsoft.AspNetCore.OData.NetTopologySuite/Common/Error.cs
@@ -0,0 +1,58 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+
+namespace Microsoft.AspNetCore.OData.NetTopologySuite.Common;
+
+[ExcludeFromCodeCoverage]
+internal static class Error
+{
+ ///
+ /// Formats the specified resource string using .
+ ///
+ /// A composite format string.
+ /// An object array that contains zero or more objects to format.
+ /// The formatted string.
+ internal static string Format(string format, params object[] args)
+ {
+ return String.Format(CultureInfo.CurrentCulture, format, args);
+ }
+
+ ///
+ /// Creates an with the provided properties.
+ ///
+ /// The name of the parameter that caused the current exception.
+ /// The logged .
+ internal static ArgumentNullException ArgumentNull(string parameterName)
+ {
+ return new ArgumentNullException(parameterName);
+ }
+
+ ///
+ /// Creates an .
+ ///
+ /// A composite format string explaining the reason for the exception.
+ /// An object array that contains zero or more objects to format.
+ /// The logged .
+ internal static InvalidOperationException InvalidOperation(string messageFormat, params object[] messageArgs)
+ {
+ return new InvalidOperationException(Error.Format(messageFormat, messageArgs));
+ }
+
+ ///
+ /// Creates an .
+ ///
+ /// A composite format string explaining the reason for the exception.
+ /// An object array that contains zero or more objects to format.
+ /// The logged .
+ internal static NotSupportedException NotSupported(string messageFormat, params object[] messageArgs)
+ {
+ return new NotSupportedException(Error.Format(messageFormat, messageArgs));
+ }
+}
diff --git a/src/Microsoft.AspNetCore.OData.NetTopologySuite/Conventions/GeographyAttributeEdmPropertyConvention.cs b/src/Microsoft.AspNetCore.OData.NetTopologySuite/Conventions/GeographyAttributeEdmPropertyConvention.cs
new file mode 100644
index 000000000..a7d245228
--- /dev/null
+++ b/src/Microsoft.AspNetCore.OData.NetTopologySuite/Conventions/GeographyAttributeEdmPropertyConvention.cs
@@ -0,0 +1,82 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using Microsoft.AspNetCore.OData.NetTopologySuite.Attributes;
+using Microsoft.AspNetCore.OData.NetTopologySuite.Common;
+using Microsoft.AspNetCore.OData.NetTopologySuite.Edm;
+using Microsoft.OData.Edm;
+using Microsoft.OData.ModelBuilder;
+using Microsoft.OData.ModelBuilder.Conventions.Attributes;
+using Geometry = NetTopologySuite.Geometries.Geometry;
+
+namespace Microsoft.AspNetCore.OData.NetTopologySuite.Conventions;
+
+///
+/// A convention that applies to individual properties decorated with the .
+/// If the property’s CLR type is a NetTopologySuite spatial type, it is mapped to the corresponding
+/// OData geography Edm type (for example, Point → Edm.GeographyPoint,
+/// LineString → Edm.GeographyLineString).
+///
+public class GeographyAttributeEdmPropertyConvention : AttributeEdmPropertyConvention
+{
+ ///
+ /// Initializes a new instance of the class.
+ /// Configures the convention to match properties annotated with and
+ /// to map NetTopologySuite spatial CLR types to their Edm.Geography* primitive kinds.
+ ///
+ public GeographyAttributeEdmPropertyConvention()
+ : base(attribute => attribute.GetType() == typeof(GeographyAttribute), allowMultiple: false)
+ {
+
+ }
+
+ ///
+ public override void Apply(PropertyConfiguration edmProperty, StructuralTypeConfiguration structuralTypeConfiguration, Attribute attribute, ODataConventionModelBuilder model)
+ {
+ if (edmProperty == null)
+ {
+ throw Error.ArgumentNull(nameof(edmProperty));
+ }
+
+ if (structuralTypeConfiguration == null)
+ {
+ throw Error.ArgumentNull(nameof(structuralTypeConfiguration));
+ }
+
+ if (model == null)
+ {
+ throw Error.ArgumentNull(nameof(model));
+ }
+
+ // If the property was added explicitly, we do not apply the convention.
+ if (edmProperty.AddedExplicitly)
+ {
+ return;
+ }
+
+ if (edmProperty is PrimitivePropertyConfiguration primitiveProperty
+ && typeof(Geometry).IsAssignableFrom(edmProperty.RelatedClrType))
+ {
+ EdmPrimitiveTypeKind? targetPrimitiveTypeKind = EdmSpatialKindMapper.GetGeographyKindForClrType(edmProperty.RelatedClrType);
+ if (targetPrimitiveTypeKind.HasValue)
+ {
+ primitiveProperty.AsSpatial(targetPrimitiveTypeKind.Value);
+ }
+ }
+ else if (edmProperty is CollectionPropertyConfiguration collectionProperty
+ && typeof(Geometry).IsAssignableFrom(collectionProperty.RelatedClrType))
+ {
+ EdmPrimitiveTypeKind? elementGeographyKind =
+ EdmSpatialKindMapper.GetGeographyKindForClrType(collectionProperty.RelatedClrType);
+
+ if (elementGeographyKind.HasValue)
+ {
+ collectionProperty.AsSpatial(elementGeographyKind.Value);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.OData.NetTopologySuite/Conventions/GeographyAttributeEdmTypeConvention.cs b/src/Microsoft.AspNetCore.OData.NetTopologySuite/Conventions/GeographyAttributeEdmTypeConvention.cs
new file mode 100644
index 000000000..8621060f8
--- /dev/null
+++ b/src/Microsoft.AspNetCore.OData.NetTopologySuite/Conventions/GeographyAttributeEdmTypeConvention.cs
@@ -0,0 +1,79 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using Microsoft.AspNetCore.OData.NetTopologySuite.Attributes;
+using Microsoft.AspNetCore.OData.NetTopologySuite.Common;
+using Microsoft.AspNetCore.OData.NetTopologySuite.Edm;
+using Microsoft.OData.Edm;
+using Microsoft.OData.ModelBuilder;
+using Microsoft.OData.ModelBuilder.Conventions.Attributes;
+using Geometry = NetTopologySuite.Geometries.Geometry;
+
+namespace Microsoft.AspNetCore.OData.NetTopologySuite.Conventions;
+
+///
+/// A convention that applies to entity or complex types decorated with the .
+/// When applied, all NetTopologySuite spatial properties on the type are mapped to the corresponding
+/// OData geography Edm types (for example, Point → Edm.GeographyPoint,
+/// LineString → Edm.GeographyLineString), rather than geometry types.
+///
+public class GeographyAttributeEdmTypeConvention : AttributeEdmTypeConvention
+{
+ ///
+ /// Initializes a new instance of the class.
+ /// Configures the convention to match types annotated with and
+ /// to map all NetTopologySuite spatial properties on those types to Edm.Geography* primitive kinds.
+ ///
+ public GeographyAttributeEdmTypeConvention()
+ : base(attribute => attribute.GetType() == typeof(GeographyAttribute), allowMultiple: false)
+ {
+
+ }
+
+ ///
+ public override void Apply(StructuralTypeConfiguration edmTypeConfiguration, ODataConventionModelBuilder model, Attribute attribute)
+ {
+ if (edmTypeConfiguration == null)
+ {
+ throw Error.ArgumentNull(nameof(edmTypeConfiguration));
+ }
+
+ if (model == null)
+ {
+ throw Error.ArgumentNull(nameof(model));
+ }
+
+ foreach (PropertyConfiguration edmProperty in edmTypeConfiguration.Properties)
+ {
+ if (edmProperty.AddedExplicitly)
+ {
+ continue;
+ }
+
+ if (edmProperty is PrimitivePropertyConfiguration primitiveProperty
+ && typeof(Geometry).IsAssignableFrom(edmProperty.RelatedClrType))
+ {
+ EdmPrimitiveTypeKind? targetPrimitiveTypeKind = EdmSpatialKindMapper.GetGeographyKindForClrType(edmProperty.RelatedClrType);
+ if (targetPrimitiveTypeKind.HasValue)
+ {
+ primitiveProperty.AsSpatial(targetPrimitiveTypeKind.Value);
+ }
+ }
+ else if (edmProperty is CollectionPropertyConfiguration collectionProperty
+ && typeof(Geometry).IsAssignableFrom(collectionProperty.RelatedClrType))
+ {
+ EdmPrimitiveTypeKind? elementGeographyKind =
+ EdmSpatialKindMapper.GetGeographyKindForClrType(collectionProperty.RelatedClrType);
+
+ if (elementGeographyKind.HasValue)
+ {
+ collectionProperty.AsSpatial(elementGeographyKind.Value);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.OData.NetTopologySuite/Edm/EdmSpatialKindMapper.cs b/src/Microsoft.AspNetCore.OData.NetTopologySuite/Edm/EdmSpatialKindMapper.cs
new file mode 100644
index 000000000..294da43c4
--- /dev/null
+++ b/src/Microsoft.AspNetCore.OData.NetTopologySuite/Edm/EdmSpatialKindMapper.cs
@@ -0,0 +1,73 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using Microsoft.OData.Edm;
+using NetTopologySuite.Geometries;
+
+namespace Microsoft.AspNetCore.OData.NetTopologySuite.Edm;
+
+///
+/// Maps NetTopologySuite geometry CLR types to their corresponding OData Edm geography primitive kinds.
+///
+///
+/// The mapping prefers the most specific known NTS shape (Point, LineString, Polygon, Multi*, GeometryCollection)
+/// and falls back to → .
+/// Returns null if the supplied type is null or not an NTS geometry type.
+///
+internal static class EdmSpatialKindMapper
+{
+ ///
+ /// Gets the Edm geography primitive kind that corresponds to the given NetTopologySuite CLR type.
+ ///
+ /// The CLR type to evaluate (e.g., Point, LineString).
+ ///
+ /// The matching for geography shapes, or null if no mapping exists.
+ ///
+ public static EdmPrimitiveTypeKind? GetGeographyKindForClrType(Type relatedClrType)
+ {
+ if (relatedClrType == null)
+ {
+ return null;
+ }
+
+ // Check most specific types first, then fall back to base Geometry.
+ if (typeof(Point).IsAssignableFrom(relatedClrType))
+ {
+ return EdmPrimitiveTypeKind.GeographyPoint;
+ }
+ if (typeof(LineString).IsAssignableFrom(relatedClrType))
+ {
+ return EdmPrimitiveTypeKind.GeographyLineString;
+ }
+ if (typeof(Polygon).IsAssignableFrom(relatedClrType))
+ {
+ return EdmPrimitiveTypeKind.GeographyPolygon;
+ }
+ if (typeof(MultiPoint).IsAssignableFrom(relatedClrType))
+ {
+ return EdmPrimitiveTypeKind.GeographyMultiPoint;
+ }
+ if (typeof(MultiLineString).IsAssignableFrom(relatedClrType))
+ {
+ return EdmPrimitiveTypeKind.GeographyMultiLineString;
+ }
+ if (typeof(MultiPolygon).IsAssignableFrom(relatedClrType))
+ {
+ return EdmPrimitiveTypeKind.GeographyMultiPolygon;
+ }
+ if (typeof(GeometryCollection).IsAssignableFrom(relatedClrType))
+ {
+ return EdmPrimitiveTypeKind.GeographyCollection;
+ }
+ if (typeof(Geometry).IsAssignableFrom(relatedClrType))
+ {
+ return EdmPrimitiveTypeKind.Geography;
+ }
+
+ return null;
+ }
+}
diff --git a/src/Microsoft.AspNetCore.OData.NetTopologySuite/Edm/ODataNetTopologySuiteTypeMapper.cs b/src/Microsoft.AspNetCore.OData.NetTopologySuite/Edm/ODataNetTopologySuiteTypeMapper.cs
new file mode 100644
index 000000000..dba8c1074
--- /dev/null
+++ b/src/Microsoft.AspNetCore.OData.NetTopologySuite/Edm/ODataNetTopologySuiteTypeMapper.cs
@@ -0,0 +1,60 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using Microsoft.AspNetCore.OData.Edm;
+using Microsoft.OData.Edm;
+using Geometry = NetTopologySuite.Geometries.Geometry;
+using GeometryCollection = NetTopologySuite.Geometries.GeometryCollection;
+using LineString = NetTopologySuite.Geometries.LineString;
+using MultiLineString = NetTopologySuite.Geometries.MultiLineString;
+using MultiPoint = NetTopologySuite.Geometries.MultiPoint;
+using MultiPolygon = NetTopologySuite.Geometries.MultiPolygon;
+using Point = NetTopologySuite.Geometries.Point;
+using Polygon = NetTopologySuite.Geometries.Polygon;
+
+namespace Microsoft.AspNetCore.OData.NetTopologySuite.Edm;
+
+///
+/// A type mapper that registers mappings between NetTopologySuite (NTS) geometry CLR types
+/// and OData Edm spatial primitive kinds.
+///
+///
+/// - Maps NTS types to Edm.Geometry* kinds (e.g., → ).
+/// - This mapper complements the default mapper by adding non-standard mappings for NTS reference types.
+/// - This mapper is attached to an as a direct-value annotation to scope it per model/route.
+///
+public class ODataNetTopologySuiteTypeMapper : DefaultODataTypeMapper
+{
+ ///
+ /// Singleton instance of .
+ ///
+ internal static readonly ODataNetTopologySuiteTypeMapper Instance;
+
+ static ODataNetTopologySuiteTypeMapper()
+ {
+ Instance = new ODataNetTopologySuiteTypeMapper();
+ }
+
+ ///
+ /// Registers the NetTopologySuite geometry CLR types as Edm spatial primitive kinds.
+ ///
+ ///
+ /// Mappings are registered as non-standard to avoid overriding the default primitive mappings and
+ /// to allow providers/conventions to influence the final Edm kind (e.g., switch to geography).
+ ///
+ protected override void RegisterSpatialMappings()
+ {
+ BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeometryPoint, isStandard: false);
+ BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeometryLineString, isStandard: false);
+ BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeometryPolygon, isStandard: false);
+ BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeometryMultiPoint, isStandard: false);
+ BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeometryMultiLineString, isStandard: false);
+ BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeometryMultiPolygon, isStandard: false);
+ BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeometryCollection, isStandard: false);
+ BuildReferenceTypeMapping(EdmPrimitiveTypeKind.Geometry, isStandard: false);
+ }
+}
diff --git a/src/Microsoft.AspNetCore.OData.NetTopologySuite/Extensions/ODataNetTopologyServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.OData.NetTopologySuite/Extensions/ODataNetTopologyServiceCollectionExtensions.cs
new file mode 100644
index 000000000..0be69fbf1
--- /dev/null
+++ b/src/Microsoft.AspNetCore.OData.NetTopologySuite/Extensions/ODataNetTopologyServiceCollectionExtensions.cs
@@ -0,0 +1,56 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using Microsoft.AspNetCore.OData.Formatter.Deserialization;
+using Microsoft.AspNetCore.OData.Formatter.Serialization;
+using Microsoft.AspNetCore.OData.NetTopologySuite.Formatter.Deserialization;
+using Microsoft.AspNetCore.OData.NetTopologySuite.Formatter.Serialization;
+using Microsoft.AspNetCore.OData.NetTopologySuite.Formatter.Serialization.Converters;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace Microsoft.AspNetCore.OData.NetTopologySuite.Extensions;
+
+///
+/// Extension methods to register NetTopologySuite spatial formatters for ASP.NET Core OData.
+///
+///
+/// This replaces the default OData spatial serializer/deserializer with implementations that read and write
+/// NetTopologySuite geometry types.
+///
+public static class ODataNetTopologyServiceCollectionExtensions
+{
+ ///
+ /// Registers NetTopologySuite spatial serializers and deserializers.
+ ///
+ /// The service collection.
+ /// The same for chaining.
+ ///
+ /// This method is idempotent: it removes existing spatial formatter registrations and adds the NTS-based ones.
+ ///
+ public static IServiceCollection AddODataNetTopologySuite(this IServiceCollection services)
+ {
+ services.RemoveAll();
+ services.AddSingleton();
+ services.RemoveAll();
+ services.AddSingleton();
+
+ services.TryAddSingleton();
+ services.TryAddEnumerable(new[]
+ {
+ ServiceDescriptor.Singleton(),
+ ServiceDescriptor.Singleton(),
+ ServiceDescriptor.Singleton(),
+ ServiceDescriptor.Singleton(),
+ ServiceDescriptor.Singleton(),
+ ServiceDescriptor.Singleton(),
+ ServiceDescriptor.Singleton(),
+ });
+
+ return services;
+ }
+}
diff --git a/src/Microsoft.AspNetCore.OData.NetTopologySuite/Extensions/ODataNetTopologySuiteEdmModelExtensions.cs b/src/Microsoft.AspNetCore.OData.NetTopologySuite/Extensions/ODataNetTopologySuiteEdmModelExtensions.cs
new file mode 100644
index 000000000..79e43521d
--- /dev/null
+++ b/src/Microsoft.AspNetCore.OData.NetTopologySuite/Extensions/ODataNetTopologySuiteEdmModelExtensions.cs
@@ -0,0 +1,38 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using Microsoft.AspNetCore.OData.Edm;
+using Microsoft.AspNetCore.OData.NetTopologySuite.Common;
+using Microsoft.AspNetCore.OData.NetTopologySuite.Edm;
+using Microsoft.AspNetCore.OData.NetTopologySuite.UriParser.Parsers;
+using Microsoft.OData.Edm;
+using Microsoft.OData.UriParser;
+
+namespace Microsoft.AspNetCore.OData.NetTopologySuite.Extensions;
+
+///
+/// Extensions to attach NetTopologySuite type mapping to an Edm model.
+///
+public static class ODataNetTopologySuiteEdmModelExtensions
+{
+ ///
+ /// Attaches the NetTopologySuite type mapper to the given Edm model.
+ ///
+ public static IEdmModel UseNetTopologySuite(this IEdmModel model)
+ {
+ if (model == null)
+ {
+ throw Error.ArgumentNull(nameof(model));
+ }
+
+ model.SetTypeMapper(ODataNetTopologySuiteTypeMapper.Instance);
+ // TODO: Make parser target Edm.Geometry* and Edm.Geography* or leave it general?
+ model.AddCustomUriLiteralParser(ODataNetTopologySuiteUriLiteralParser.Instance);
+
+ return model;
+ }
+}
diff --git a/src/Microsoft.AspNetCore.OData.NetTopologySuite/Extensions/ODataNetTopologySuiteODataConventionModelBuilderExtensions.cs b/src/Microsoft.AspNetCore.OData.NetTopologySuite/Extensions/ODataNetTopologySuiteODataConventionModelBuilderExtensions.cs
new file mode 100644
index 000000000..037a9142e
--- /dev/null
+++ b/src/Microsoft.AspNetCore.OData.NetTopologySuite/Extensions/ODataNetTopologySuiteODataConventionModelBuilderExtensions.cs
@@ -0,0 +1,40 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using Microsoft.AspNetCore.OData.NetTopologySuite.Common;
+using Microsoft.AspNetCore.OData.NetTopologySuite.Conventions;
+using Microsoft.AspNetCore.OData.NetTopologySuite.Providers;
+using Microsoft.OData.ModelBuilder;
+
+namespace Microsoft.AspNetCore.OData.NetTopologySuite.Extensions;
+
+///
+/// Extension methods to configure NetTopologySuite integration on an .
+///
+public static class ODataNetTopologySuiteODataConventionModelBuilderExtensions
+{
+ ///
+ /// Adds the NetTopologySuite Edm type mapping provider and necessary spatial conventions.
+ ///
+ public static ODataConventionModelBuilder UseNetTopologySuite(this ODataConventionModelBuilder builder)
+ {
+ if (builder == null)
+ {
+ throw Error.ArgumentNull(nameof(builder));
+ }
+
+ // Add Edm type mapping provider for NetTopologySuite spatial types
+ builder.AddEdmTypeMappingProvider(new ODataNetTopologySuiteEdmTypeMappingProvider());
+
+ // Add geography conventions for properties and types
+ builder.AddModelConventions(
+ new GeographyAttributeEdmPropertyConvention(),
+ new GeographyAttributeEdmTypeConvention());
+
+ return builder;
+ }
+}
diff --git a/src/Microsoft.AspNetCore.OData.NetTopologySuite/Formatter/Deserialization/ODataSpatialNetTopologySuiteDeserializer.cs b/src/Microsoft.AspNetCore.OData.NetTopologySuite/Formatter/Deserialization/ODataSpatialNetTopologySuiteDeserializer.cs
new file mode 100644
index 000000000..ca8a5c9ab
--- /dev/null
+++ b/src/Microsoft.AspNetCore.OData.NetTopologySuite/Formatter/Deserialization/ODataSpatialNetTopologySuiteDeserializer.cs
@@ -0,0 +1,201 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using System.Collections.ObjectModel;
+using Microsoft.AspNetCore.OData.Formatter.Deserialization;
+using Microsoft.AspNetCore.OData.NetTopologySuite.Common;
+using Microsoft.OData;
+using Microsoft.OData.Edm;
+using Microsoft.OData.UriParser;
+using Microsoft.Spatial;
+using NetTopologySuite.Geometries;
+
+namespace Microsoft.AspNetCore.OData.NetTopologySuite.Formatter.Deserialization;
+
+///
+/// Represents an that can read OData spatial types.
+///
+internal class ODataSpatialNetTopologySuiteDeserializer : ODataSpatialDeserializer
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ODataSpatialNetTopologySuiteDeserializer()
+ : base()
+ {
+ }
+
+ public override async Task