diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
index ae4cd58f96..84913d6fdc 100644
--- a/.devcontainer/docker-compose.yml
+++ b/.devcontainer/docker-compose.yml
@@ -3,7 +3,7 @@ version: '3'
services:
npgsql-dev:
# Source for tags: https://mcr.microsoft.com/v2/dotnet/sdk/tags/list
- image: mcr.microsoft.com/dotnet/sdk:8.0.100-preview.6
+ image: mcr.microsoft.com/dotnet/sdk:8.0.100-preview.7
volumes:
- ..:/workspace:cached
tty: true
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 065f6a2924..66e0ff5bd6 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -15,7 +15,7 @@ concurrency:
cancel-in-progress: true
env:
- dotnet_sdk_version: '8.0.100-preview.6.23330.14'
+ dotnet_sdk_version: '8.0.100-rc.1.23463.5'
postgis_version: 3
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
# Windows comes with PG pre-installed, and defines the PGPASSWORD environment variable. Remove it as it interferes
@@ -315,7 +315,6 @@ jobs:
run: |
if [ -z "${{ matrix.pg_prerelease }}" ]; then
dotnet test -c ${{ matrix.config }} -f ${{ matrix.test_tfm }} test/Npgsql.PluginTests --logger "GitHubActions;report-warnings=false"
- dotnet test -c ${{ matrix.config }} -f ${{ matrix.test_tfm }} test/Npgsql.NodaTime.Tests --logger "GitHubActions;report-warnings=false"
fi
shell: bash
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index a4b0f44bbb..93c46a180c 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -32,7 +32,7 @@ concurrency:
cancel-in-progress: true
env:
- dotnet_sdk_version: '8.0.100-preview.6.23330.14'
+ dotnet_sdk_version: '8.0.100-rc.1.23463.5'
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
jobs:
diff --git a/.github/workflows/native-aot.yml b/.github/workflows/native-aot.yml
index 6b58f5f7cd..1c6ecce4ba 100644
--- a/.github/workflows/native-aot.yml
+++ b/.github/workflows/native-aot.yml
@@ -15,31 +15,32 @@ concurrency:
cancel-in-progress: true
env:
- dotnet_sdk_version: '8.0.100-preview.6.23330.14'
+ dotnet_sdk_version: '8.0.100-rc.1.23463.5'
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
- nuget_config: |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ # Uncomment and edit the following to use nightly/preview builds
+# nuget_config: |
+#
+#
+#
+#
+#
+#
+#
+#
+#
+#
+#
+#
+#
+#
+#
+#
+#
+#
+#
+#
+#
+#
jobs:
build:
runs-on: ${{ matrix.os }}
@@ -68,8 +69,8 @@ jobs:
dotnet-version: |
${{ env.dotnet_sdk_version }}
- - name: Setup nuget config
- run: echo "$nuget_config" > NuGet.config
+# - name: Setup nuget config
+# run: echo "$nuget_config" > NuGet.config
- name: Setup Native AOT prerequisites
run: sudo apt-get install clang zlib1g-dev
@@ -108,6 +109,20 @@ jobs:
path: "test/Npgsql.NativeAotTests/obj/Release/net8.0/linux-x64/native/Npgsql.NativeAotTests.mstat"
retention-days: 3
+ - name: Upload codedgen dgml
+ uses: actions/upload-artifact@v3.1.2
+ with:
+ name: npgsql.codegen.dgml.xml
+ path: "test/Npgsql.NativeAotTests/obj/Release/net8.0/linux-x64/native/Npgsql.NativeAotTests.codegen.dgml.xml"
+ retention-days: 3
+
+ - name: Upload scan dgml
+ uses: actions/upload-artifact@v3.1.2
+ with:
+ name: npgsql.scan.dgml.xml
+ path: "test/Npgsql.NativeAotTests/obj/Release/net8.0/linux-x64/native/Npgsql.NativeAotTests.scan.dgml.xml"
+ retention-days: 3
+
- name: Assert binary size
run: |
size="$(ls -l test/Npgsql.NativeAotTests/bin/Release/net8.0/linux-x64/native/Npgsql.NativeAotTests | cut -d ' ' -f 5)"
diff --git a/.github/workflows/rich-code-nav.yml b/.github/workflows/rich-code-nav.yml
index bc2db9b271..e47a8a2adb 100644
--- a/.github/workflows/rich-code-nav.yml
+++ b/.github/workflows/rich-code-nav.yml
@@ -9,7 +9,7 @@ on:
- '*'
env:
- dotnet_sdk_version: '8.0.100-preview.6.23330.14'
+ dotnet_sdk_version: '8.0.100-rc.1.23463.5'
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
jobs:
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 41220a98ac..2648b64175 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,27 +1,40 @@
+
+ 8.0.0-rc.1.23419.4
+ $(SystemVersion)
+
+
-
-
-
-
+
+
-
-
-
-
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
-
-
+
+
@@ -29,7 +42,7 @@
-
+
@@ -39,20 +52,4 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Npgsql.sln b/Npgsql.sln
index 007681d5bb..80ef02c3a8 100644
--- a/Npgsql.sln
+++ b/Npgsql.sln
@@ -37,8 +37,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Npgsql.SourceGenerators", "src\Npgsql.SourceGenerators\Npgsql.SourceGenerators.csproj", "{63026A19-60B8-4906-81CB-216F30E8094B}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Npgsql.NodaTime.Tests", "test\Npgsql.NodaTime.Tests\Npgsql.NodaTime.Tests.csproj", "{C00D2EB1-5719-4372-9E1C-5ED05DC23A00}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Npgsql.OpenTelemetry", "src\Npgsql.OpenTelemetry\Npgsql.OpenTelemetry.csproj", "{DA29F063-1828-47D8-B051-800AF7C9A0BE}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Github", "Github", "{BA7B6F53-D24D-45AC-927A-266857EA8D1E}"
@@ -144,14 +142,6 @@ Global
{63026A19-60B8-4906-81CB-216F30E8094B}.Release|Any CPU.Build.0 = Release|Any CPU
{63026A19-60B8-4906-81CB-216F30E8094B}.Release|x86.ActiveCfg = Release|Any CPU
{63026A19-60B8-4906-81CB-216F30E8094B}.Release|x86.Build.0 = Release|Any CPU
- {C00D2EB1-5719-4372-9E1C-5ED05DC23A00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C00D2EB1-5719-4372-9E1C-5ED05DC23A00}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C00D2EB1-5719-4372-9E1C-5ED05DC23A00}.Debug|x86.ActiveCfg = Debug|Any CPU
- {C00D2EB1-5719-4372-9E1C-5ED05DC23A00}.Debug|x86.Build.0 = Debug|Any CPU
- {C00D2EB1-5719-4372-9E1C-5ED05DC23A00}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C00D2EB1-5719-4372-9E1C-5ED05DC23A00}.Release|Any CPU.Build.0 = Release|Any CPU
- {C00D2EB1-5719-4372-9E1C-5ED05DC23A00}.Release|x86.ActiveCfg = Release|Any CPU
- {C00D2EB1-5719-4372-9E1C-5ED05DC23A00}.Release|x86.Build.0 = Release|Any CPU
{DA29F063-1828-47D8-B051-800AF7C9A0BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DA29F063-1828-47D8-B051-800AF7C9A0BE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DA29F063-1828-47D8-B051-800AF7C9A0BE}.Debug|x86.ActiveCfg = Debug|Any CPU
@@ -199,7 +189,6 @@ Global
{F7C53EBD-0075-474F-A083-419257D04080} = {8537E50E-CF7F-49CB-B4EF-3E2A1B11F050}
{A77E5FAF-D775-4AB4-8846-8965C2104E60} = {ED612DB1-AB32-4603-95E7-891BACA71C39}
{63026A19-60B8-4906-81CB-216F30E8094B} = {8537E50E-CF7F-49CB-B4EF-3E2A1B11F050}
- {C00D2EB1-5719-4372-9E1C-5ED05DC23A00} = {ED612DB1-AB32-4603-95E7-891BACA71C39}
{DA29F063-1828-47D8-B051-800AF7C9A0BE} = {8537E50E-CF7F-49CB-B4EF-3E2A1B11F050}
{BA7B6F53-D24D-45AC-927A-266857EA8D1E} = {004A2E0F-D34A-44D4-8DF0-D2BC63B57073}
{B58E12EB-E43D-4D77-894E-5157D2269836} = {8537E50E-CF7F-49CB-B4EF-3E2A1B11F050}
diff --git a/src/Npgsql.GeoJSON/Internal/CrsMap.WellKnown.cs b/src/Npgsql.GeoJSON/CrsMap.WellKnown.cs
similarity index 99%
rename from src/Npgsql.GeoJSON/Internal/CrsMap.WellKnown.cs
rename to src/Npgsql.GeoJSON/CrsMap.WellKnown.cs
index 9d733830ea..14da2f893e 100644
--- a/src/Npgsql.GeoJSON/Internal/CrsMap.WellKnown.cs
+++ b/src/Npgsql.GeoJSON/CrsMap.WellKnown.cs
@@ -1,6 +1,6 @@
-namespace Npgsql.GeoJSON.Internal;
+namespace Npgsql.GeoJSON;
-readonly partial struct CrsMap
+public partial class CrsMap
{
///
/// These entries came from spatial_res_sys. They are used to elide memory allocations
@@ -586,4 +586,4 @@ readonly partial struct CrsMap
new(32766, 32766, "EPSG"),
new(900913, 900913, "spatialreferencing.org"),
};
-}
\ No newline at end of file
+}
diff --git a/src/Npgsql.GeoJSON/CrsMap.cs b/src/Npgsql.GeoJSON/CrsMap.cs
new file mode 100644
index 0000000000..dd556d9b33
--- /dev/null
+++ b/src/Npgsql.GeoJSON/CrsMap.cs
@@ -0,0 +1,59 @@
+
+namespace Npgsql.GeoJSON;
+
+///
+/// A map of entries that map the authority to the inclusive range of SRID.
+///
+public partial class CrsMap
+{
+ readonly CrsMapEntry[]? _overriden;
+
+ internal CrsMap(CrsMapEntry[]? overriden)
+ => _overriden = overriden;
+
+ internal string? GetAuthority(int srid)
+ => GetAuthority(_overriden, srid) ?? GetAuthority(WellKnown, srid);
+
+ static string? GetAuthority(CrsMapEntry[]? entries, int srid)
+ {
+ if (entries == null)
+ return null;
+
+ var left = 0;
+ var right = entries.Length;
+ while (left <= right)
+ {
+ var middle = left + (right - left) / 2;
+ var entry = entries[middle];
+
+ if (srid < entry.MinSrid)
+ right = middle - 1;
+ else
+ if (srid > entry.MaxSrid)
+ left = middle + 1;
+ else
+ return entry.Authority;
+ }
+
+ return null;
+ }
+}
+
+///
+/// An entry which maps the authority to the inclusive range of SRID.
+///
+readonly struct CrsMapEntry
+{
+ internal readonly int MinSrid;
+ internal readonly int MaxSrid;
+ internal readonly string? Authority;
+
+ internal CrsMapEntry(int minSrid, int maxSrid, string? authority)
+ {
+ MinSrid = minSrid;
+ MaxSrid = maxSrid;
+ Authority = authority != null
+ ? string.IsInterned(authority) ?? authority
+ : null;
+ }
+}
diff --git a/src/Npgsql.GeoJSON/CrsMapExtensions.cs b/src/Npgsql.GeoJSON/CrsMapExtensions.cs
new file mode 100644
index 0000000000..329b7d9265
--- /dev/null
+++ b/src/Npgsql.GeoJSON/CrsMapExtensions.cs
@@ -0,0 +1,50 @@
+using System.Threading.Tasks;
+using Npgsql.GeoJSON.Internal;
+
+namespace Npgsql.GeoJSON;
+
+///
+/// Extensions for getting a CrsMap from a database.
+///
+public static class CrsMapExtensions
+{
+ ///
+ /// Gets the full crs details from the database.
+ ///
+ ///
+ public static async Task GetCrsMapAsync(this NpgsqlDataSource dataSource)
+ {
+ var builder = new CrsMapBuilder();
+ using var cmd = GetCsrCommand(dataSource);
+ await using var reader = await cmd.ExecuteReaderAsync();
+
+ while (await reader.ReadAsync())
+ builder.Add(new CrsMapEntry(reader.GetInt32(0), reader.GetInt32(1), reader.GetString(2)));
+
+ return builder.Build();
+ }
+
+ ///
+ /// Gets the full crs details from the database.
+ ///
+ ///
+ public static CrsMap GetCrsMap(this NpgsqlDataSource dataSource)
+ {
+ var builder = new CrsMapBuilder();
+ using var cmd = GetCsrCommand(dataSource);
+ using var reader = cmd.ExecuteReader();
+
+ while (reader.Read())
+ builder.Add(new CrsMapEntry(reader.GetInt32(0), reader.GetInt32(1), reader.GetString(2)));
+
+ return builder.Build();
+ }
+
+ static NpgsqlCommand GetCsrCommand(NpgsqlDataSource dataSource)
+ => dataSource.CreateCommand("""
+ SELECT min(srid), max(srid), auth_name
+ FROM(SELECT srid, auth_name, srid - rank() OVER(PARTITION BY auth_name ORDER BY srid) AS range FROM spatial_ref_sys) AS s
+ GROUP BY range, auth_name
+ ORDER BY 1;
+ """);
+}
diff --git a/src/Npgsql.GeoJSON/Internal/CrsMap.cs b/src/Npgsql.GeoJSON/Internal/CrsMapBuilder.cs
similarity index 52%
rename from src/Npgsql.GeoJSON/Internal/CrsMap.cs
rename to src/Npgsql.GeoJSON/Internal/CrsMapBuilder.cs
index aa7dc58e2d..44829761c9 100644
--- a/src/Npgsql.GeoJSON/Internal/CrsMap.cs
+++ b/src/Npgsql.GeoJSON/Internal/CrsMapBuilder.cs
@@ -2,25 +2,6 @@
namespace Npgsql.GeoJSON.Internal;
-///
-/// An entry which maps the authority to the inclusive range of SRID.
-///
-readonly struct CrsMapEntry
-{
- internal readonly int MinSrid;
- internal readonly int MaxSrid;
- internal readonly string? Authority;
-
- internal CrsMapEntry(int minSrid, int maxSrid, string? authority)
- {
- MinSrid = minSrid;
- MaxSrid = maxSrid;
- Authority = authority != null
- ? string.IsInterned(authority) ?? authority
- : null;
- }
-}
-
struct CrsMapBuilder
{
CrsMapEntry[] _overrides;
@@ -71,38 +52,3 @@ internal CrsMap Build()
return new CrsMap(_overrides);
}
}
-
-readonly partial struct CrsMap
-{
- readonly CrsMapEntry[]? _overriden;
-
- internal CrsMap(CrsMapEntry[]? overriden)
- => _overriden = overriden;
-
- internal string? GetAuthority(int srid)
- => GetAuthority(_overriden, srid) ?? GetAuthority(WellKnown, srid);
-
- static string? GetAuthority(CrsMapEntry[]? entries, int srid)
- {
- if (entries == null)
- return null;
-
- var left = 0;
- var right = entries.Length;
- while (left <= right)
- {
- var middle = left + (right - left) / 2;
- var entry = entries[middle];
-
- if (srid < entry.MinSrid)
- right = middle - 1;
- else
- if (srid > entry.MaxSrid)
- left = middle + 1;
- else
- return entry.Authority;
- }
-
- return null;
- }
-}
\ No newline at end of file
diff --git a/src/Npgsql.GeoJSON/Internal/GeoJSONConverter.cs b/src/Npgsql.GeoJSON/Internal/GeoJSONConverter.cs
new file mode 100644
index 0000000000..2f6ece1fd8
--- /dev/null
+++ b/src/Npgsql.GeoJSON/Internal/GeoJSONConverter.cs
@@ -0,0 +1,748 @@
+using System;
+using System.Buffers.Binary;
+using System.Collections.Concurrent;
+using System.Collections.ObjectModel;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using GeoJSON.Net;
+using GeoJSON.Net.CoordinateReferenceSystem;
+using GeoJSON.Net.Geometry;
+using Npgsql.Internal;
+
+namespace Npgsql.GeoJSON.Internal;
+
+sealed class GeoJSONConverter : PgStreamingConverter where T : IGeoJSONObject
+{
+ readonly ConcurrentDictionary _cachedCrs = new();
+ readonly GeoJSONOptions _options;
+ readonly Func _getCrs;
+
+ public GeoJSONConverter(GeoJSONOptions options, CrsMap crsMap)
+ {
+ _options = options;
+ _getCrs = GetCrs(
+ crsMap,
+ _cachedCrs,
+ crsType: _options & (GeoJSONOptions.ShortCRS | GeoJSONOptions.LongCRS)
+ );
+ }
+
+ bool BoundingBox => (_options & GeoJSONOptions.BoundingBox) != 0;
+
+ public override T Read(PgReader reader)
+ => (T)GeoJSONConverter.Read(async: false, reader, BoundingBox ? new BoundingBoxBuilder() : null, _getCrs, CancellationToken.None).GetAwaiter().GetResult();
+
+ public override async ValueTask ReadAsync(PgReader reader, CancellationToken cancellationToken = default)
+ => (T)await GeoJSONConverter.Read(async: true, reader, BoundingBox ? new BoundingBoxBuilder() : null, _getCrs, cancellationToken).ConfigureAwait(false);
+
+ public override Size GetSize(SizeContext context, T value, ref object? writeState)
+ => GeoJSONConverter.GetSize(context, value, ref writeState);
+
+ public override void Write(PgWriter writer, T value)
+ => GeoJSONConverter.Write(async: false, writer, value, CancellationToken.None).GetAwaiter().GetResult();
+
+ public override ValueTask WriteAsync(PgWriter writer, T value, CancellationToken cancellationToken = default)
+ => GeoJSONConverter.Write(async: true, writer, value, CancellationToken.None);
+
+ static Func GetCrs(CrsMap crsMap, ConcurrentDictionary cachedCrs, GeoJSONOptions crsType)
+ => srid =>
+ {
+ if (crsType == GeoJSONOptions.None)
+ return null;
+
+#if NETSTANDARD2_0
+ return cachedCrs.GetOrAdd(srid, srid =>
+ {
+ var authority = crsMap.GetAuthority(srid);
+
+ return authority is null
+ ? throw new InvalidOperationException($"SRID {srid} unknown in spatial_ref_sys table")
+ : new NamedCRS(crsType == GeoJSONOptions.LongCRS
+ ? "urn:ogc:def:crs:" + authority + "::" + srid
+ : authority + ":" + srid);
+ });
+#else
+ return cachedCrs.GetOrAdd(srid, static (srid, state) =>
+ {
+ var (crsMap, crsType) = state;
+ var authority = crsMap.GetAuthority(srid);
+
+ return authority is null
+ ? throw new InvalidOperationException($"SRID {srid} unknown in spatial_ref_sys table")
+ : new NamedCRS(crsType == GeoJSONOptions.LongCRS
+ ? "urn:ogc:def:crs:" + authority + "::" + srid
+ : authority + ":" + srid);
+ }, (crsMap, crsType));
+#endif
+ };
+}
+
+static class GeoJSONConverter
+{
+ public static async ValueTask Read(bool async, PgReader reader, BoundingBoxBuilder? boundingBox, Func getCrs, CancellationToken cancellationToken)
+ {
+ var geometry = await Core(async, reader, boundingBox, getCrs, cancellationToken).ConfigureAwait(false);
+ geometry.BoundingBoxes = boundingBox?.Build();
+ return geometry;
+
+ static async ValueTask Core(bool async, PgReader reader, BoundingBoxBuilder? boundingbox, Func getCrs, CancellationToken cancellationToken)
+ {
+ if (reader.ShouldBuffer(SizeOfHeader))
+ await reader.BufferData(async, SizeOfHeader, cancellationToken).ConfigureAwait(false);
+
+ var littleEndian = reader.ReadByte() > 0;
+ var type = (EwkbGeometryType)ReadUInt32(littleEndian);
+
+ GeoJSONObject geometry;
+ NamedCRS? crs = null;
+
+ if (HasSrid(type))
+ {
+ if (reader.ShouldBuffer(sizeof(int)))
+ await reader.BufferData(async, sizeof(int), cancellationToken).ConfigureAwait(false);
+ crs = getCrs(ReadInt32(littleEndian));
+ }
+
+ switch (type & EwkbGeometryType.BaseType)
+ {
+ case EwkbGeometryType.Point:
+ {
+ if (SizeOfPoint(type) is var size && reader.ShouldBuffer(size))
+ await reader.BufferData(async, size, cancellationToken).ConfigureAwait(false);
+
+ var position = ReadPosition(reader, type, littleEndian);
+ boundingbox?.Accumulate(position);
+ geometry = new Point(position);
+ break;
+ }
+
+ case EwkbGeometryType.LineString:
+ {
+ if (reader.ShouldBuffer(SizeOfLength))
+ await reader.BufferData(async, SizeOfLength, cancellationToken).ConfigureAwait(false);
+
+ var coordinates = new Position[ReadInt32(littleEndian)];
+ for (var i = 0; i < coordinates.Length; ++i)
+ {
+ if (SizeOfPoint(type) is var size && reader.ShouldBuffer(size))
+ await reader.BufferData(async, size, cancellationToken).ConfigureAwait(false);
+
+ var position = ReadPosition(reader, type, littleEndian);
+ boundingbox?.Accumulate(position);
+ coordinates[i] = position;
+ }
+ geometry = new LineString(coordinates);
+ break;
+ }
+
+ case EwkbGeometryType.Polygon:
+ {
+ if (reader.ShouldBuffer(SizeOfLength))
+ await reader.BufferData(async, SizeOfLength, cancellationToken).ConfigureAwait(false);
+
+ var lines = new LineString[ReadInt32(littleEndian)];
+ for (var i = 0; i < lines.Length; ++i)
+ {
+ if (reader.ShouldBuffer(SizeOfLength))
+ await reader.BufferData(async, SizeOfLength, cancellationToken).ConfigureAwait(false);
+
+ var coordinates = new Position[ReadInt32(littleEndian)];
+ for (var j = 0; j < coordinates.Length; ++j)
+ {
+ if (SizeOfPoint(type) is var size && reader.ShouldBuffer(size))
+ await reader.BufferData(async, size, cancellationToken).ConfigureAwait(false);
+
+ var position = ReadPosition(reader, type, littleEndian);
+ boundingbox?.Accumulate(position);
+ coordinates[j] = position;
+ }
+ lines[i] = new LineString(coordinates);
+ }
+ geometry = new Polygon(lines);
+ break;
+ }
+
+ case EwkbGeometryType.MultiPoint:
+ {
+ if (reader.ShouldBuffer(SizeOfLength))
+ await reader.BufferData(async, SizeOfLength, cancellationToken).ConfigureAwait(false);
+
+ var points = new Point[ReadInt32(littleEndian)];
+ for (var i = 0; i < points.Length; ++i)
+ {
+ if (SizeOfHeader + SizeOfPoint(type) is var size && reader.ShouldBuffer(size))
+ await reader.BufferData(async, size, cancellationToken).ConfigureAwait(false);
+
+ if (async)
+ await reader.ConsumeAsync(SizeOfHeader, cancellationToken).ConfigureAwait(false);
+ else
+ reader.Consume(SizeOfHeader);
+
+ var position = ReadPosition(reader, type, littleEndian);
+ boundingbox?.Accumulate(position);
+ points[i] = new Point(position);
+ }
+ geometry = new MultiPoint(points);
+ break;
+ }
+
+ case EwkbGeometryType.MultiLineString:
+ {
+ if (reader.ShouldBuffer(SizeOfLength))
+ await reader.BufferData(async, SizeOfLength, cancellationToken).ConfigureAwait(false);
+
+ var lines = new LineString[ReadInt32(littleEndian)];
+ for (var i = 0; i < lines.Length; ++i)
+ {
+ if (reader.ShouldBuffer(SizeOfHeaderWithLength))
+ await reader.BufferData(async, SizeOfHeaderWithLength, cancellationToken).ConfigureAwait(false);
+
+ if (async)
+ await reader.ConsumeAsync(SizeOfHeader, cancellationToken).ConfigureAwait(false);
+ else
+ reader.Consume(SizeOfHeader);
+
+ var coordinates = new Position[ReadInt32(littleEndian)];
+ for (var j = 0; j < coordinates.Length; ++j)
+ {
+ if (SizeOfPoint(type) is var size && reader.ShouldBuffer(size))
+ await reader.BufferData(async, size, cancellationToken).ConfigureAwait(false);
+ var position = ReadPosition(reader, type, littleEndian);
+ boundingbox?.Accumulate(position);
+ coordinates[j] = position;
+ }
+ lines[i] = new LineString(coordinates);
+ }
+ geometry = new MultiLineString(lines);
+ break;
+ }
+
+ case EwkbGeometryType.MultiPolygon:
+ {
+ if (reader.ShouldBuffer(SizeOfLength))
+ await reader.BufferData(async, SizeOfLength, cancellationToken).ConfigureAwait(false);
+
+ var polygons = new Polygon[ReadInt32(littleEndian)];
+ for (var i = 0; i < polygons.Length; ++i)
+ {
+ if (reader.ShouldBuffer(SizeOfHeaderWithLength))
+ await reader.BufferData(async, SizeOfHeaderWithLength, cancellationToken).ConfigureAwait(false);
+
+ if (async)
+ await reader.ConsumeAsync(SizeOfHeader, cancellationToken).ConfigureAwait(false);
+ else
+ reader.Consume(SizeOfHeader);
+
+ var lines = new LineString[ReadInt32(littleEndian)];
+ for (var j = 0; j < lines.Length; ++j)
+ {
+ if (reader.ShouldBuffer(SizeOfLength))
+ await reader.BufferData(async, SizeOfLength, cancellationToken).ConfigureAwait(false);
+ var coordinates = new Position[ReadInt32(littleEndian)];
+ for (var k = 0; k < coordinates.Length; ++k)
+ {
+ if (SizeOfPoint(type) is var size && reader.ShouldBuffer(size))
+ await reader.BufferData(async, size, cancellationToken).ConfigureAwait(false);
+ var position = ReadPosition(reader, type, littleEndian);
+ boundingbox?.Accumulate(position);
+ coordinates[k] = position;
+ }
+ lines[j] = new LineString(coordinates);
+ }
+ polygons[i] = new Polygon(lines);
+ }
+ geometry = new MultiPolygon(polygons);
+ break;
+ }
+
+ case EwkbGeometryType.GeometryCollection:
+ {
+ if (reader.ShouldBuffer(SizeOfLength))
+ await reader.BufferData(async, SizeOfLength, cancellationToken).ConfigureAwait(false);
+
+ var elements = new IGeometryObject[ReadInt32(littleEndian)];
+ for (var i = 0; i < elements.Length; ++i)
+ elements[i] = (IGeometryObject)await Core(async, reader, boundingbox, getCrs, cancellationToken).ConfigureAwait(false);
+ geometry = new GeometryCollection(elements);
+ break;
+ }
+
+ default:
+ throw UnknownPostGisType();
+ }
+
+ geometry.CRS = crs;
+ return geometry;
+
+ int ReadInt32(bool littleEndian)
+ => littleEndian ? BinaryPrimitives.ReverseEndianness(reader.ReadInt32()) : reader.ReadInt32();
+ uint ReadUInt32(bool littleEndian)
+ => littleEndian ? BinaryPrimitives.ReverseEndianness(reader.ReadUInt32()) : reader.ReadUInt32();
+ }
+
+ static Position ReadPosition(PgReader reader, EwkbGeometryType type, bool littleEndian)
+ {
+ var position = new Position(
+ longitude: ReadDouble(littleEndian),
+ latitude: ReadDouble(littleEndian),
+ altitude: HasZ(type) ? reader.ReadDouble() : null);
+ if (HasM(type)) ReadDouble(littleEndian);
+ return position;
+
+ double ReadDouble(bool littleEndian)
+ => littleEndian
+ // Netstandard is missing ReverseEndianness apis for double.
+ ? Unsafe.As(ref Unsafe.AsRef(
+ BinaryPrimitives.ReverseEndianness(Unsafe.As(ref Unsafe.AsRef(reader.ReadDouble())))))
+ : reader.ReadDouble();
+ }
+ }
+
+ public static Size GetSize(SizeContext context, IGeoJSONObject value, ref object? writeState)
+ => value.Type switch
+ {
+ GeoJSONObjectType.Point => GetSize((Point)value),
+ GeoJSONObjectType.LineString => GetSize((LineString)value),
+ GeoJSONObjectType.Polygon => GetSize((Polygon)value),
+ GeoJSONObjectType.MultiPoint => GetSize((MultiPoint)value),
+ GeoJSONObjectType.MultiLineString => GetSize((MultiLineString)value),
+ GeoJSONObjectType.MultiPolygon => GetSize((MultiPolygon)value),
+ GeoJSONObjectType.GeometryCollection => GetSize(context, (GeometryCollection)value, ref writeState),
+ _ => throw UnknownPostGisType()
+ };
+
+ static bool NotValid(ReadOnlyCollection coordinates, out bool hasZ)
+ {
+ if (coordinates.Count == 0)
+ hasZ = false;
+ else
+ {
+ hasZ = HasZ(coordinates[0]);
+ for (var i = 1; i < coordinates.Count; ++i)
+ if (HasZ(coordinates[i]) != hasZ) return true;
+ }
+ return false;
+ }
+
+ static Size GetSize(Point value)
+ {
+ var length = Size.Create(SizeOfHeader + SizeOfPoint(HasZ(value.Coordinates)));
+ if (GetSrid(value.CRS) != 0)
+ length = length.Combine(sizeof(int));
+
+ return length;
+ }
+
+ static Size GetSize(LineString value)
+ {
+ var coordinates = value.Coordinates;
+ if (NotValid(coordinates, out var hasZ))
+ throw AllOrNoneCoordiantesMustHaveZ(nameof(LineString));
+
+ var length = Size.Create(SizeOfHeaderWithLength + coordinates.Count * SizeOfPoint(hasZ));
+ if (GetSrid(value.CRS) != 0)
+ length = length.Combine(sizeof(int));
+
+ return length;
+ }
+
+ static Size GetSize(Polygon value)
+ {
+ var lines = value.Coordinates;
+ var length = Size.Create(SizeOfHeaderWithLength + SizeOfLength * lines.Count);
+ if (GetSrid(value.CRS) != 0)
+ length = length.Combine(sizeof(int));
+
+ var hasZ = false;
+ for (var i = 0; i < lines.Count; ++i)
+ {
+ var coordinates = lines[i].Coordinates;
+ if (NotValid(coordinates, out var lineHasZ))
+ throw AllOrNoneCoordiantesMustHaveZ(nameof(Polygon));
+
+ if (hasZ != lineHasZ)
+ {
+ if (i == 0) hasZ = lineHasZ;
+ else throw AllOrNoneCoordiantesMustHaveZ(nameof(LineString));
+ }
+
+ length = length.Combine(coordinates.Count * SizeOfPoint(hasZ));
+ }
+
+ return length;
+ }
+
+ static Size GetSize(MultiPoint value)
+ {
+ var length = Size.Create(SizeOfHeaderWithLength);
+ if (GetSrid(value.CRS) != 0)
+ length = length.Combine(sizeof(int));
+
+ var coordinates = value.Coordinates;
+ foreach (var t in coordinates)
+ length = length.Combine(GetSize(t));
+
+ return length;
+ }
+
+ static Size GetSize(MultiLineString value)
+ {
+ var length = Size.Create(SizeOfHeaderWithLength);
+ if (GetSrid(value.CRS) != 0)
+ length = length.Combine(sizeof(int));
+
+ var coordinates = value.Coordinates;
+ foreach (var t in coordinates)
+ length = length.Combine(GetSize(t));
+
+ return length;
+ }
+
+ static Size GetSize(MultiPolygon value)
+ {
+ var length = Size.Create(SizeOfHeaderWithLength);
+ if (GetSrid(value.CRS) != 0)
+ length = length.Combine(sizeof(int));
+
+ var coordinates = value.Coordinates;
+ foreach (var t in coordinates)
+ length = length.Combine(GetSize(t));
+
+ return length;
+ }
+
+ static Size GetSize(SizeContext context, GeometryCollection value, ref object? writeState)
+ {
+ var length = Size.Create(SizeOfHeaderWithLength);
+ if (GetSrid(value.CRS) != 0)
+ length = length.Combine(sizeof(int));
+
+ var geometries = value.Geometries;
+ foreach (var t in geometries)
+ length = length.Combine(GetSize(context, (IGeoJSONObject)t, ref writeState));
+
+ return length;
+ }
+
+ public static ValueTask Write(bool async, PgWriter writer, IGeoJSONObject value, CancellationToken cancellationToken = default)
+ => value.Type switch
+ {
+ GeoJSONObjectType.Point => Write(async, writer, (Point)value, cancellationToken),
+ GeoJSONObjectType.LineString => Write(async, writer, (LineString)value, cancellationToken),
+ GeoJSONObjectType.Polygon => Write(async, writer, (Polygon)value, cancellationToken),
+ GeoJSONObjectType.MultiPoint => Write(async, writer, (MultiPoint)value, cancellationToken),
+ GeoJSONObjectType.MultiLineString => Write(async, writer, (MultiLineString)value, cancellationToken),
+ GeoJSONObjectType.MultiPolygon => Write(async, writer, (MultiPolygon)value, cancellationToken),
+ GeoJSONObjectType.GeometryCollection => Write(async, writer, (GeometryCollection)value, cancellationToken),
+ _ => throw UnknownPostGisType()
+ };
+
+ static async ValueTask Write(bool async, PgWriter writer, Point value, CancellationToken cancellationToken)
+ {
+ var type = EwkbGeometryType.Point;
+ var size = SizeOfHeader;
+ var srid = GetSrid(value.CRS);
+ if (srid != 0)
+ {
+ size += sizeof(int);
+ type |= EwkbGeometryType.HasSrid;
+ }
+
+ if (writer.ShouldFlush(size))
+ await writer.Flush(async, cancellationToken).ConfigureAwait(false);
+
+ writer.WriteByte(0); // Most significant byte first
+ writer.WriteInt32((int)type);
+
+ if (srid != 0)
+ writer.WriteInt32(srid);
+
+ await WritePosition(async, writer, value.Coordinates, cancellationToken).ConfigureAwait(false);
+ }
+
+ static async ValueTask Write(bool async, PgWriter writer, LineString value, CancellationToken cancellationToken)
+ {
+ var type = EwkbGeometryType.LineString;
+ var size = SizeOfHeaderWithLength;
+ var srid = GetSrid(value.CRS);
+ if (srid != 0)
+ {
+ size += sizeof(int);
+ type |= EwkbGeometryType.HasSrid;
+ }
+
+ if (writer.ShouldFlush(size))
+ await writer.Flush(async, cancellationToken).ConfigureAwait(false);
+
+ var coordinates = value.Coordinates;
+
+ writer.WriteByte(0); // Most significant byte first
+ writer.WriteInt32((int)type);
+ writer.WriteInt32(coordinates.Count);
+
+ if (srid != 0)
+ writer.WriteInt32(srid);
+
+ foreach (var t in coordinates)
+ await WritePosition(async, writer, t, cancellationToken).ConfigureAwait(false);
+ }
+
+ static async ValueTask Write(bool async, PgWriter writer, Polygon value, CancellationToken cancellationToken)
+ {
+ var type = EwkbGeometryType.Polygon;
+ var size = SizeOfHeaderWithLength;
+ var srid = GetSrid(value.CRS);
+ if (srid != 0)
+ {
+ size += sizeof(int);
+ type |= EwkbGeometryType.HasSrid;
+ }
+
+ if (writer.ShouldFlush(size))
+ await writer.Flush(async, cancellationToken).ConfigureAwait(false);
+
+ var lines = value.Coordinates;
+
+ writer.WriteByte(0); // Most significant byte first
+ writer.WriteInt32((int)type);
+ writer.WriteInt32(lines.Count);
+
+ if (srid != 0)
+ writer.WriteInt32(srid);
+
+ foreach (var t in lines)
+ {
+ if (writer.ShouldFlush(SizeOfLength))
+ await writer.Flush(async, cancellationToken).ConfigureAwait(false);
+ var coordinates = t.Coordinates;
+ writer.WriteInt32(coordinates.Count);
+ foreach (var t1 in coordinates)
+ await WritePosition(async, writer, t1, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ static async ValueTask Write(bool async, PgWriter writer, MultiPoint value, CancellationToken cancellationToken)
+ {
+ var type = EwkbGeometryType.MultiPoint;
+ var size = SizeOfHeaderWithLength;
+ var srid = GetSrid(value.CRS);
+ if (srid != 0)
+ {
+ size += sizeof(int);
+ type |= EwkbGeometryType.HasSrid;
+ }
+
+ if (writer.ShouldFlush(size))
+ await writer.Flush(async, cancellationToken).ConfigureAwait(false);
+
+ var coordinates = value.Coordinates;
+
+ writer.WriteByte(0); // Most significant byte first
+ writer.WriteInt32((int)type);
+ writer.WriteInt32(coordinates.Count);
+
+ if (srid != 0)
+ writer.WriteInt32(srid);
+
+ foreach (var t in coordinates)
+ await Write(async, writer, t, cancellationToken).ConfigureAwait(false);
+ }
+
+ static async ValueTask Write(bool async, PgWriter writer, MultiLineString value, CancellationToken cancellationToken)
+ {
+ var type = EwkbGeometryType.MultiLineString;
+ var size = SizeOfHeaderWithLength;
+ var srid = GetSrid(value.CRS);
+ if (srid != 0)
+ {
+ size += sizeof(int);
+ type |= EwkbGeometryType.HasSrid;
+ }
+
+ if (writer.ShouldFlush(size))
+ await writer.Flush(async, cancellationToken).ConfigureAwait(false);
+
+ var coordinates = value.Coordinates;
+
+ writer.WriteByte(0); // Most significant byte first
+ writer.WriteInt32((int)type);
+ writer.WriteInt32(coordinates.Count);
+
+ if (srid != 0)
+ writer.WriteInt32(srid);
+
+ foreach (var t in coordinates)
+ await Write(async, writer, t, cancellationToken).ConfigureAwait(false);
+ }
+
+ static async ValueTask Write(bool async, PgWriter writer, MultiPolygon value, CancellationToken cancellationToken)
+ {
+ var type = EwkbGeometryType.MultiPolygon;
+ var size = SizeOfHeaderWithLength;
+ var srid = GetSrid(value.CRS);
+ if (srid != 0)
+ {
+ size += sizeof(int);
+ type |= EwkbGeometryType.HasSrid;
+ }
+
+ if (writer.ShouldFlush(size))
+ await writer.Flush(async, cancellationToken).ConfigureAwait(false);
+
+ var coordinates = value.Coordinates;
+
+ writer.WriteByte(0); // Most significant byte first
+ writer.WriteInt32((int)type);
+ writer.WriteInt32(coordinates.Count);
+
+ if (srid != 0)
+ writer.WriteInt32(srid);
+ foreach (var t in coordinates)
+ await Write(async, writer, t, cancellationToken).ConfigureAwait(false);
+ }
+
+ static async ValueTask Write(bool async, PgWriter writer, GeometryCollection value, CancellationToken cancellationToken)
+ {
+ var type = EwkbGeometryType.GeometryCollection;
+ var size = SizeOfHeaderWithLength;
+ var srid = GetSrid(value.CRS);
+ if (srid != 0)
+ {
+ size += sizeof(int);
+ type |= EwkbGeometryType.HasSrid;
+ }
+
+ if (writer.ShouldFlush(size))
+ await writer.Flush(async, cancellationToken).ConfigureAwait(false);
+
+ var geometries = value.Geometries;
+
+ writer.WriteByte(0); // Most significant byte first
+ writer.WriteInt32((int)type);
+ writer.WriteInt32(geometries.Count);
+
+ if (srid != 0)
+ writer.WriteInt32(srid);
+
+ foreach (var t in geometries)
+ await Write(async, writer, (IGeoJSONObject)t, cancellationToken).ConfigureAwait(false);
+ }
+
+ static async ValueTask WritePosition(bool async, PgWriter writer, IPosition coordinate, CancellationToken cancellationToken)
+ {
+ var altitude = coordinate.Altitude;
+ if (SizeOfPoint(altitude.HasValue) is var size && writer.ShouldFlush(size))
+ await writer.Flush(async, cancellationToken).ConfigureAwait(false);
+
+ writer.WriteDouble(coordinate.Longitude);
+ writer.WriteDouble(coordinate.Latitude);
+ if (altitude.HasValue)
+ writer.WriteDouble(altitude.Value);
+ }
+
+ static ValueTask BufferData(this PgReader reader, bool async, int byteCount, CancellationToken cancellationToken)
+ {
+ if (async)
+ return reader.BufferAsync(byteCount, cancellationToken);
+
+ reader.Buffer(byteCount);
+ return new();
+ }
+
+ static ValueTask Flush(this PgWriter writer, bool async, CancellationToken cancellationToken)
+ {
+ if (async)
+ return writer.FlushAsync(cancellationToken);
+
+ writer.Flush();
+ return new();
+ }
+
+ static bool HasSrid(EwkbGeometryType type)
+ => (type & EwkbGeometryType.HasSrid) != 0;
+
+ static bool HasZ(EwkbGeometryType type)
+ => (type & EwkbGeometryType.HasZ) != 0;
+
+ static bool HasM(EwkbGeometryType type)
+ => (type & EwkbGeometryType.HasM) != 0;
+
+ static bool HasZ(IPosition coordinates)
+ => coordinates.Altitude.HasValue;
+
+ const int SizeOfLength = sizeof(int);
+ const int SizeOfHeader = sizeof(byte) + sizeof(EwkbGeometryType);
+ const int SizeOfHeaderWithLength = SizeOfHeader + SizeOfLength;
+ const int SizeOfPoint2D = 2 * sizeof(double);
+ const int SizeOfPoint3D = 3 * sizeof(double);
+
+ static int SizeOfPoint(bool hasZ)
+ => hasZ ? SizeOfPoint3D : SizeOfPoint2D;
+
+ static int SizeOfPoint(EwkbGeometryType type)
+ {
+ var size = SizeOfPoint2D;
+ if (HasZ(type))
+ size += sizeof(double);
+ if (HasM(type))
+ size += sizeof(double);
+ return size;
+ }
+
+ static Exception UnknownPostGisType()
+ => throw new InvalidOperationException("Invalid PostGIS type");
+
+ static Exception AllOrNoneCoordiantesMustHaveZ(string typeName)
+ => new ArgumentException($"The Z coordinate must be specified for all or none elements of {typeName}");
+
+ static int GetSrid(ICRSObject crs)
+ {
+ if (crs is null or UnspecifiedCRS)
+ return 0;
+
+ var namedCrs = crs as NamedCRS;
+ if (namedCrs == null)
+ throw new NotSupportedException("The LinkedCRS class isn't supported");
+
+ if (namedCrs.Properties.TryGetValue("name", out var value) && value != null)
+ {
+ var name = value.ToString()!;
+ if (string.Equals(name, "urn:ogc:def:crs:OGC::CRS84", StringComparison.Ordinal))
+ return 4326;
+
+ var index = name.LastIndexOf(':');
+ if (index != -1 && int.TryParse(name.Substring(index + 1), out var srid))
+ return srid;
+
+ throw new FormatException("The specified CRS isn't properly named");
+ }
+
+ return 0;
+ }
+}
+
+///
+/// Represents the identifier of the Well Known Binary representation of a geographical feature specified by the OGC.
+/// http://portal.opengeospatial.org/files/?artifact_id=13227 Chapter 6.3.2.7
+///
+[Flags]
+enum EwkbGeometryType : uint
+{
+ // Types
+ Point = 1,
+ LineString = 2,
+ Polygon = 3,
+ MultiPoint = 4,
+ MultiLineString = 5,
+ MultiPolygon = 6,
+ GeometryCollection = 7,
+
+ // Masks
+ BaseType = Point | LineString | Polygon | MultiPoint | MultiLineString | MultiPolygon | GeometryCollection,
+
+ // Flags
+ HasSrid = 0x20000000,
+ HasM = 0x40000000,
+ HasZ = 0x80000000
+}
diff --git a/src/Npgsql.GeoJSON/Internal/GeoJSONHandler.cs b/src/Npgsql.GeoJSON/Internal/GeoJSONHandler.cs
deleted file mode 100644
index ba040ed79d..0000000000
--- a/src/Npgsql.GeoJSON/Internal/GeoJSONHandler.cs
+++ /dev/null
@@ -1,722 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Collections.ObjectModel;
-using System.Threading;
-using System.Threading.Tasks;
-using GeoJSON.Net;
-using GeoJSON.Net.CoordinateReferenceSystem;
-using GeoJSON.Net.Geometry;
-using Npgsql.BackendMessages;
-using Npgsql.Internal;
-using Npgsql.Internal.TypeHandling;
-using Npgsql.PostgresTypes;
-
-namespace Npgsql.GeoJSON.Internal;
-
-sealed partial class GeoJsonHandler : NpgsqlTypeHandler,
- INpgsqlTypeHandler, INpgsqlTypeHandler,
- INpgsqlTypeHandler, INpgsqlTypeHandler,
- INpgsqlTypeHandler, INpgsqlTypeHandler,
- INpgsqlTypeHandler,
- INpgsqlTypeHandler,
- INpgsqlTypeHandler
-{
- readonly GeoJSONOptions _options;
- readonly CrsMap _crsMap;
- readonly ConcurrentDictionary _cachedCrs = new();
-
- internal GeoJsonHandler(PostgresType postgresType, GeoJSONOptions options, CrsMap crsMap)
- : base(postgresType)
- {
- _options = options;
- _crsMap = crsMap;
- }
-
- GeoJSONOptions CrsType => _options & (GeoJSONOptions.ShortCRS | GeoJSONOptions.LongCRS);
-
- bool BoundingBox => (_options & GeoJSONOptions.BoundingBox) != 0;
-
- static bool HasSrid(EwkbGeometryType type)
- => (type & EwkbGeometryType.HasSrid) != 0;
-
- static bool HasZ(EwkbGeometryType type)
- => (type & EwkbGeometryType.HasZ) != 0;
-
- static bool HasM(EwkbGeometryType type)
- => (type & EwkbGeometryType.HasM) != 0;
-
- static bool HasZ(IPosition coordinates)
- => coordinates.Altitude.HasValue;
-
- const int SizeOfLength = sizeof(int);
- const int SizeOfHeader = sizeof(byte) + sizeof(EwkbGeometryType);
- const int SizeOfHeaderWithLength = SizeOfHeader + SizeOfLength;
- const int SizeOfPoint2D = 2 * sizeof(double);
- const int SizeOfPoint3D = 3 * sizeof(double);
-
- static int SizeOfPoint(bool hasZ)
- => hasZ ? SizeOfPoint3D : SizeOfPoint2D;
-
- static int SizeOfPoint(EwkbGeometryType type)
- {
- var size = SizeOfPoint2D;
- if (HasZ(type))
- size += sizeof(double);
- if (HasM(type))
- size += sizeof(double);
- return size;
- }
-
- #region Throw
-
- static Exception UnknownPostGisType()
- => throw new InvalidOperationException("Invalid PostGIS type");
-
- static Exception AllOrNoneCoordiantesMustHaveZ(NpgsqlParameter? parameter, string typeName)
- => parameter is null
- ? new ArgumentException($"The Z coordinate must be specified for all or none elements of {typeName}")
- : new ArgumentException($"The Z coordinate must be specified for all or none elements of {typeName} in the {parameter.ParameterName} parameter", parameter.ParameterName);
-
- #endregion
-
- #region Read
-
- public override ValueTask Read(NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription = null)
- => ReadGeometry(buf, async);
-
- async ValueTask INpgsqlTypeHandler.Read(NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription)
- => (Point)await ReadGeometry(buf, async);
-
- async ValueTask INpgsqlTypeHandler.Read(NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription)
- => (LineString)await ReadGeometry(buf, async);
-
- async ValueTask INpgsqlTypeHandler.Read(NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription)
- => (Polygon)await ReadGeometry(buf, async);
-
- async ValueTask INpgsqlTypeHandler.Read(NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription)
- => (MultiPoint)await ReadGeometry(buf, async);
-
- async ValueTask INpgsqlTypeHandler.Read(NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription)
- => (MultiLineString)await ReadGeometry(buf, async);
-
- async ValueTask INpgsqlTypeHandler.Read(NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription)
- => (MultiPolygon)await ReadGeometry(buf, async);
-
- async ValueTask INpgsqlTypeHandler.Read(NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription)
- => (GeometryCollection)await ReadGeometry(buf, async);
-
- async ValueTask INpgsqlTypeHandler.Read(NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription)
- => await ReadGeometry(buf, async);
-
- async ValueTask INpgsqlTypeHandler.Read(NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription)
- => (IGeometryObject)await ReadGeometry(buf, async);
-
- async ValueTask ReadGeometry(NpgsqlReadBuffer buf, bool async)
- {
- var boundingBox = BoundingBox ? new BoundingBoxBuilder() : null;
- var geometry = await ReadGeometryCore(buf, async, boundingBox);
-
- geometry.BoundingBoxes = boundingBox?.Build();
- return geometry;
- }
-
- async ValueTask ReadGeometryCore(NpgsqlReadBuffer buf, bool async, BoundingBoxBuilder? boundingBox)
- {
- await buf.Ensure(SizeOfHeader, async);
- var littleEndian = buf.ReadByte() > 0;
- var type = (EwkbGeometryType)buf.ReadUInt32(littleEndian);
-
- GeoJSONObject geometry;
- NamedCRS? crs = null;
-
- if (HasSrid(type))
- {
- await buf.Ensure(4, async);
- crs = GetCrs(buf.ReadInt32(littleEndian));
- }
-
- switch (type & EwkbGeometryType.BaseType)
- {
- case EwkbGeometryType.Point:
- {
- await buf.Ensure(SizeOfPoint(type), async);
- var position = ReadPosition(buf, type, littleEndian);
- boundingBox?.Accumulate(position);
- geometry = new Point(position);
- break;
- }
-
- case EwkbGeometryType.LineString:
- {
- await buf.Ensure(SizeOfLength, async);
- var coordinates = new Position[buf.ReadInt32(littleEndian)];
- for (var i = 0; i < coordinates.Length; ++i)
- {
- await buf.Ensure(SizeOfPoint(type), async);
- var position = ReadPosition(buf, type, littleEndian);
- boundingBox?.Accumulate(position);
- coordinates[i] = position;
- }
- geometry = new LineString(coordinates);
- break;
- }
-
- case EwkbGeometryType.Polygon:
- {
- await buf.Ensure(SizeOfLength, async);
- var lines = new LineString[buf.ReadInt32(littleEndian)];
- for (var i = 0; i < lines.Length; ++i)
- {
- await buf.Ensure(SizeOfLength, async);
- var coordinates = new Position[buf.ReadInt32(littleEndian)];
- for (var j = 0; j < coordinates.Length; ++j)
- {
- await buf.Ensure(SizeOfPoint(type), async);
- var position = ReadPosition(buf, type, littleEndian);
- boundingBox?.Accumulate(position);
- coordinates[j] = position;
- }
- lines[i] = new LineString(coordinates);
- }
- geometry = new Polygon(lines);
- break;
- }
-
- case EwkbGeometryType.MultiPoint:
- {
- await buf.Ensure(SizeOfLength, async);
- var points = new Point[buf.ReadInt32(littleEndian)];
- for (var i = 0; i < points.Length; ++i)
- {
- await buf.Ensure(SizeOfHeader + SizeOfPoint(type), async);
- await buf.Skip(SizeOfHeader, async);
- var position = ReadPosition(buf, type, littleEndian);
- boundingBox?.Accumulate(position);
- points[i] = new Point(position);
- }
- geometry = new MultiPoint(points);
- break;
- }
-
- case EwkbGeometryType.MultiLineString:
- {
- await buf.Ensure(SizeOfLength, async);
- var lines = new LineString[buf.ReadInt32(littleEndian)];
- for (var i = 0; i < lines.Length; ++i)
- {
- await buf.Ensure(SizeOfHeaderWithLength, async);
- await buf.Skip(SizeOfHeader, async);
- var coordinates = new Position[buf.ReadInt32(littleEndian)];
- for (var j = 0; j < coordinates.Length; ++j)
- {
- await buf.Ensure(SizeOfPoint(type), async);
- var position = ReadPosition(buf, type, littleEndian);
- boundingBox?.Accumulate(position);
- coordinates[j] = position;
- }
- lines[i] = new LineString(coordinates);
- }
- geometry = new MultiLineString(lines);
- break;
- }
-
- case EwkbGeometryType.MultiPolygon:
- {
- await buf.Ensure(SizeOfLength, async);
- var polygons = new Polygon[buf.ReadInt32(littleEndian)];
- for (var i = 0; i < polygons.Length; ++i)
- {
- await buf.Ensure(SizeOfHeaderWithLength, async);
- await buf.Skip(SizeOfHeader, async);
- var lines = new LineString[buf.ReadInt32(littleEndian)];
- for (var j = 0; j < lines.Length; ++j)
- {
- await buf.Ensure(SizeOfLength, async);
- var coordinates = new Position[buf.ReadInt32(littleEndian)];
- for (var k = 0; k < coordinates.Length; ++k)
- {
- await buf.Ensure(SizeOfPoint(type), async);
- var position = ReadPosition(buf, type, littleEndian);
- boundingBox?.Accumulate(position);
- coordinates[k] = position;
- }
- lines[j] = new LineString(coordinates);
- }
- polygons[i] = new Polygon(lines);
- }
- geometry = new MultiPolygon(polygons);
- break;
- }
-
- case EwkbGeometryType.GeometryCollection:
- {
- await buf.Ensure(SizeOfLength, async);
- var elements = new IGeometryObject[buf.ReadInt32(littleEndian)];
- for (var i = 0; i < elements.Length; ++i)
- elements[i] = (IGeometryObject)await ReadGeometryCore(buf, async, boundingBox);
- geometry = new GeometryCollection(elements);
- break;
- }
-
- default:
- throw UnknownPostGisType();
- }
-
- geometry.CRS = crs;
- return geometry;
- }
-
- static Position ReadPosition(NpgsqlReadBuffer buf, EwkbGeometryType type, bool littleEndian)
- {
- var position = new Position(
- longitude: buf.ReadDouble(littleEndian),
- latitude: buf.ReadDouble(littleEndian),
- altitude: HasZ(type) ? buf.ReadDouble() : (double?)null);
- if (HasM(type)) buf.ReadDouble(littleEndian);
- return position;
- }
-
- #endregion
-
- #region Write
-
- public override int ValidateAndGetLength(GeoJSONObject value, ref NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter)
- => value.Type switch
- {
- GeoJSONObjectType.Point => ValidateAndGetLength((Point)value, ref lengthCache, parameter),
- GeoJSONObjectType.LineString => ValidateAndGetLength((LineString)value, ref lengthCache, parameter),
- GeoJSONObjectType.Polygon => ValidateAndGetLength((Polygon)value, ref lengthCache, parameter),
- GeoJSONObjectType.MultiPoint => ValidateAndGetLength((MultiPoint)value, ref lengthCache, parameter),
- GeoJSONObjectType.MultiLineString => ValidateAndGetLength((MultiLineString)value, ref lengthCache, parameter),
- GeoJSONObjectType.MultiPolygon => ValidateAndGetLength((MultiPolygon)value, ref lengthCache, parameter),
- GeoJSONObjectType.GeometryCollection => ValidateAndGetLength((GeometryCollection)value, ref lengthCache, parameter),
- _ => throw UnknownPostGisType()
- };
-
- public int ValidateAndGetLength(Point value, ref NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter)
- {
- var length = SizeOfHeader + SizeOfPoint(HasZ(value.Coordinates));
- if (GetSrid(value.CRS) != 0)
- length += sizeof(int);
-
- return length;
- }
-
- public int ValidateAndGetLength(LineString value, ref NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter)
- {
- var coordinates = value.Coordinates;
- if (NotValid(coordinates, out var hasZ))
- throw AllOrNoneCoordiantesMustHaveZ(parameter, nameof(LineString));
-
- var length = SizeOfHeaderWithLength + coordinates.Count * SizeOfPoint(hasZ);
- if (GetSrid(value.CRS) != 0)
- length += sizeof(int);
-
- return length;
- }
-
- public int ValidateAndGetLength(Polygon value, ref NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter)
- {
- var lines = value.Coordinates;
- var length = SizeOfHeaderWithLength + SizeOfLength * lines.Count;
- if (GetSrid(value.CRS) != 0)
- length += sizeof(int);
-
- var hasZ = false;
- for (var i = 0; i < lines.Count; ++i)
- {
- var coordinates = lines[i].Coordinates;
- if (NotValid(coordinates, out var lineHasZ))
- throw AllOrNoneCoordiantesMustHaveZ(parameter, nameof(Polygon));
-
- if (hasZ != lineHasZ)
- {
- if (i == 0) hasZ = lineHasZ;
- else throw AllOrNoneCoordiantesMustHaveZ(parameter, nameof(LineString));
- }
-
- length += coordinates.Count * SizeOfPoint(hasZ);
- }
-
- return length;
- }
-
- static bool NotValid(ReadOnlyCollection coordinates, out bool hasZ)
- {
- if (coordinates.Count == 0)
- hasZ = false;
- else
- {
- hasZ = HasZ(coordinates[0]);
- for (var i = 1; i < coordinates.Count; ++i)
- if (HasZ(coordinates[i]) != hasZ) return true;
- }
- return false;
- }
-
- public int ValidateAndGetLength(MultiPoint value, ref NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter)
- {
- var length = SizeOfHeaderWithLength;
- if (GetSrid(value.CRS) != 0)
- length += sizeof(int);
-
- var coordinates = value.Coordinates;
- for (var i = 0; i < coordinates.Count; ++i)
- length += ValidateAndGetLength(coordinates[i], ref lengthCache, parameter);
-
- return length;
- }
-
- public int ValidateAndGetLength(MultiLineString value, ref NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter)
- {
- var length = SizeOfHeaderWithLength;
- if (GetSrid(value.CRS) != 0)
- length += sizeof(int);
-
- var coordinates = value.Coordinates;
- for (var i = 0; i < coordinates.Count; ++i)
- length += ValidateAndGetLength(coordinates[i], ref lengthCache, parameter);
-
- return length;
- }
-
- public int ValidateAndGetLength(MultiPolygon value, ref NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter)
- {
- var length = SizeOfHeaderWithLength;
- if (GetSrid(value.CRS) != 0)
- length += sizeof(int);
-
- var coordinates = value.Coordinates;
- for (var i = 0; i < coordinates.Count; ++i)
- length += ValidateAndGetLength(coordinates[i], ref lengthCache, parameter);
-
- return length;
- }
-
- public int ValidateAndGetLength(GeometryCollection value, ref NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter)
- {
- var length = SizeOfHeaderWithLength;
- if (GetSrid(value.CRS) != 0)
- length += sizeof(int);
-
- var geometries = value.Geometries;
- for (var i = 0; i < geometries.Count; ++i)
- length += ValidateAndGetLength((GeoJSONObject)geometries[i], ref lengthCache, parameter);
-
- return length;
- }
-
- int INpgsqlTypeHandler.ValidateAndGetLength(IGeoJSONObject value, ref NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter)
- => ValidateAndGetLength((GeoJSONObject)value, ref lengthCache, parameter);
-
- int INpgsqlTypeHandler.ValidateAndGetLength(IGeometryObject value, ref NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter)
- => ValidateAndGetLength((GeoJSONObject)value, ref lengthCache, parameter);
-
- public override Task Write(GeoJSONObject value, NpgsqlWriteBuffer buf, NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter, bool async, CancellationToken cancellationToken = default)
- => value.Type switch
- {
- GeoJSONObjectType.Point => Write((Point)value, buf, lengthCache, parameter, async, cancellationToken),
- GeoJSONObjectType.LineString => Write((LineString)value, buf, lengthCache, parameter, async, cancellationToken),
- GeoJSONObjectType.Polygon => Write((Polygon)value, buf, lengthCache, parameter, async, cancellationToken),
- GeoJSONObjectType.MultiPoint => Write((MultiPoint)value, buf, lengthCache, parameter, async, cancellationToken),
- GeoJSONObjectType.MultiLineString => Write((MultiLineString)value, buf, lengthCache, parameter, async, cancellationToken),
- GeoJSONObjectType.MultiPolygon => Write((MultiPolygon)value, buf, lengthCache, parameter, async, cancellationToken),
- GeoJSONObjectType.GeometryCollection => Write((GeometryCollection)value, buf, lengthCache, parameter, async, cancellationToken),
- _ => throw UnknownPostGisType()
- };
-
- public async Task Write(Point value, NpgsqlWriteBuffer buf, NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter, bool async, CancellationToken cancellationToken = default)
- {
- var type = EwkbGeometryType.Point;
- var size = SizeOfHeader;
- var srid = GetSrid(value.CRS);
- if (srid != 0)
- {
- size += sizeof(int);
- type |= EwkbGeometryType.HasSrid;
- }
-
- if (buf.WriteSpaceLeft < size)
- await buf.Flush(async, cancellationToken);
-
- buf.WriteByte(0); // Most significant byte first
- buf.WriteInt32((int)type);
-
- if (srid != 0)
- buf.WriteInt32(srid);
-
- await WritePosition(value.Coordinates, buf, async, cancellationToken);
- }
-
- public async Task Write(LineString value, NpgsqlWriteBuffer buf, NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter, bool async, CancellationToken cancellationToken = default)
- {
- var type = EwkbGeometryType.LineString;
- var size = SizeOfHeaderWithLength;
- var srid = GetSrid(value.CRS);
- if (srid != 0)
- {
- size += sizeof(int);
- type |= EwkbGeometryType.HasSrid;
- }
-
- if (buf.WriteSpaceLeft < size)
- await buf.Flush(async, cancellationToken);
-
- var coordinates = value.Coordinates;
-
- buf.WriteByte(0); // Most significant byte first
- buf.WriteInt32((int)type);
- buf.WriteInt32(coordinates.Count);
-
- if (srid != 0)
- buf.WriteInt32(srid);
-
- for (var i = 0; i < coordinates.Count; ++i)
- await WritePosition(coordinates[i], buf, async, cancellationToken);
- }
-
- public async Task Write(Polygon value, NpgsqlWriteBuffer buf, NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter, bool async, CancellationToken cancellationToken = default)
- {
- var type = EwkbGeometryType.Polygon;
- var size = SizeOfHeaderWithLength;
- var srid = GetSrid(value.CRS);
- if (srid != 0)
- {
- size += sizeof(int);
- type |= EwkbGeometryType.HasSrid;
- }
-
- if (buf.WriteSpaceLeft < size)
- await buf.Flush(async, cancellationToken);
-
- var lines = value.Coordinates;
-
- buf.WriteByte(0); // Most significant byte first
- buf.WriteInt32((int)type);
- buf.WriteInt32(lines.Count);
-
- if (srid != 0)
- buf.WriteInt32(srid);
-
- for (var i = 0; i < lines.Count; ++i)
- {
- if (buf.WriteSpaceLeft < SizeOfLength)
- await buf.Flush(async, cancellationToken);
- var coordinates = lines[i].Coordinates;
- buf.WriteInt32(coordinates.Count);
- for (var j = 0; j < coordinates.Count; ++j)
- await WritePosition(coordinates[j], buf, async, cancellationToken);
- }
- }
-
- public async Task Write(MultiPoint value, NpgsqlWriteBuffer buf, NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter, bool async, CancellationToken cancellationToken = default)
- {
- var type = EwkbGeometryType.MultiPoint;
- var size = SizeOfHeaderWithLength;
- var srid = GetSrid(value.CRS);
- if (srid != 0)
- {
- size += sizeof(int);
- type |= EwkbGeometryType.HasSrid;
- }
-
- if (buf.WriteSpaceLeft < size)
- await buf.Flush(async, cancellationToken);
-
- var coordinates = value.Coordinates;
-
- buf.WriteByte(0); // Most significant byte first
- buf.WriteInt32((int)type);
- buf.WriteInt32(coordinates.Count);
-
- if (srid != 0)
- buf.WriteInt32(srid);
-
- for (var i = 0; i < coordinates.Count; ++i)
- await Write(coordinates[i], buf, lengthCache, parameter, async, cancellationToken);
- }
-
- public async Task Write(MultiLineString value, NpgsqlWriteBuffer buf, NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter, bool async, CancellationToken cancellationToken = default)
- {
- var type = EwkbGeometryType.MultiLineString;
- var size = SizeOfHeaderWithLength;
- var srid = GetSrid(value.CRS);
- if (srid != 0)
- {
- size += sizeof(int);
- type |= EwkbGeometryType.HasSrid;
- }
-
- if (buf.WriteSpaceLeft < size)
- await buf.Flush(async, cancellationToken);
-
- var coordinates = value.Coordinates;
-
- buf.WriteByte(0); // Most significant byte first
- buf.WriteInt32((int)type);
- buf.WriteInt32(coordinates.Count);
-
- if (srid != 0)
- buf.WriteInt32(srid);
-
- for (var i = 0; i < coordinates.Count; ++i)
- await Write(coordinates[i], buf, lengthCache, parameter, async, cancellationToken);
- }
-
- public async Task Write(MultiPolygon value, NpgsqlWriteBuffer buf, NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter, bool async, CancellationToken cancellationToken = default)
- {
- var type = EwkbGeometryType.MultiPolygon;
- var size = SizeOfHeaderWithLength;
- var srid = GetSrid(value.CRS);
- if (srid != 0)
- {
- size += sizeof(int);
- type |= EwkbGeometryType.HasSrid;
- }
-
- if (buf.WriteSpaceLeft < size)
- await buf.Flush(async, cancellationToken);
-
- var coordinates = value.Coordinates;
-
- buf.WriteByte(0); // Most significant byte first
- buf.WriteInt32((int)type);
- buf.WriteInt32(coordinates.Count);
-
- if (srid != 0)
- buf.WriteInt32(srid);
- for (var i = 0; i < coordinates.Count; ++i)
- await Write(coordinates[i], buf, lengthCache, parameter, async, cancellationToken);
- }
-
- public async Task Write(GeometryCollection value, NpgsqlWriteBuffer buf, NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter, bool async, CancellationToken cancellationToken = default)
- {
- var type = EwkbGeometryType.GeometryCollection;
- var size = SizeOfHeaderWithLength;
- var srid = GetSrid(value.CRS);
- if (srid != 0)
- {
- size += sizeof(int);
- type |= EwkbGeometryType.HasSrid;
- }
-
- if (buf.WriteSpaceLeft < size)
- await buf.Flush(async, cancellationToken);
-
- var geometries = value.Geometries;
-
- buf.WriteByte(0); // Most significant byte first
- buf.WriteInt32((int)type);
- buf.WriteInt32(geometries.Count);
-
- if (srid != 0)
- buf.WriteInt32(srid);
-
- for (var i = 0; i < geometries.Count; ++i)
- await Write((GeoJSONObject) geometries[i], buf, lengthCache, parameter, async, cancellationToken);
- }
-
- Task INpgsqlTypeHandler.Write(IGeoJSONObject value, NpgsqlWriteBuffer buf, NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter, bool async, CancellationToken cancellationToken)
- => Write((GeoJSONObject)value, buf, lengthCache, parameter, async, cancellationToken);
-
- Task INpgsqlTypeHandler.Write(IGeometryObject value, NpgsqlWriteBuffer buf, NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter, bool async, CancellationToken cancellationToken)
- => Write((GeoJSONObject)value, buf, lengthCache, parameter, async, cancellationToken);
-
- static async Task WritePosition(IPosition coordinate, NpgsqlWriteBuffer buf, bool async, CancellationToken cancellationToken = default)
- {
- var altitude = coordinate.Altitude;
- if (buf.WriteSpaceLeft < SizeOfPoint(altitude.HasValue))
- await buf.Flush(async, cancellationToken);
- buf.WriteDouble(coordinate.Longitude);
- buf.WriteDouble(coordinate.Latitude);
- if (altitude.HasValue)
- buf.WriteDouble(altitude.Value);
- }
-
- #endregion
-
- #region Crs
-
- NamedCRS? GetCrs(int srid)
- {
- var crsType = CrsType;
- if (crsType == GeoJSONOptions.None)
- return null;
-
-#if NETSTANDARD2_0
- return _cachedCrs.GetOrAdd(srid, srid =>
- {
- var authority = _crsMap.GetAuthority(srid);
-
- return authority is null
- ? throw new InvalidOperationException($"SRID {srid} unknown in spatial_ref_sys table")
- : new NamedCRS(crsType == GeoJSONOptions.LongCRS
- ? "urn:ogc:def:crs:" + authority + "::" + srid
- : authority + ":" + srid);
- });
-#else
- return _cachedCrs.GetOrAdd(srid, static (srid, me) =>
- {
- var authority = me._crsMap.GetAuthority(srid);
-
- return authority is null
- ? throw new InvalidOperationException($"SRID {srid} unknown in spatial_ref_sys table")
- : new NamedCRS(me.CrsType == GeoJSONOptions.LongCRS
- ? "urn:ogc:def:crs:" + authority + "::" + srid
- : authority + ":" + srid);
- }, this);
-#endif
- }
-
- static int GetSrid(ICRSObject crs)
- {
- if (crs == null || crs is UnspecifiedCRS)
- return 0;
-
- var namedCrs = crs as NamedCRS;
- if (namedCrs == null)
- throw new NotSupportedException("The LinkedCRS class isn't supported");
-
- if (namedCrs.Properties.TryGetValue("name", out var value) && value != null)
- {
- var name = value.ToString()!;
- if (string.Equals(name, "urn:ogc:def:crs:OGC::CRS84", StringComparison.Ordinal))
- return 4326;
-
- var index = name.LastIndexOf(':');
- if (index != -1 && int.TryParse(name.Substring(index + 1), out var srid))
- return srid;
-
- throw new FormatException("The specified CRS isn't properly named");
- }
-
- return 0;
- }
-
- #endregion
-}
-
-///
-/// Represents the identifier of the Well Known Binary representation of a geographical feature specified by the OGC.
-/// http://portal.opengeospatial.org/files/?artifact_id=13227 Chapter 6.3.2.7
-///
-[Flags]
-enum EwkbGeometryType : uint
-{
- // Types
- Point = 1,
- LineString = 2,
- Polygon = 3,
- MultiPoint = 4,
- MultiLineString = 5,
- MultiPolygon = 6,
- GeometryCollection = 7,
-
- // Masks
- BaseType = Point | LineString | Polygon | MultiPoint | MultiLineString | MultiPolygon | GeometryCollection,
-
- // Flags
- HasSrid = 0x20000000,
- HasM = 0x40000000,
- HasZ = 0x80000000
-}
diff --git a/src/Npgsql.GeoJSON/Internal/GeoJSONTypeHandlerResolver.cs b/src/Npgsql.GeoJSON/Internal/GeoJSONTypeHandlerResolver.cs
deleted file mode 100644
index a937c1d62b..0000000000
--- a/src/Npgsql.GeoJSON/Internal/GeoJSONTypeHandlerResolver.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Data;
-using GeoJSON.Net;
-using GeoJSON.Net.Geometry;
-using Newtonsoft.Json;
-using Npgsql.Internal;
-using Npgsql.Internal.TypeHandling;
-using Npgsql.PostgresTypes;
-using Npgsql.TypeMapping;
-using NpgsqlTypes;
-
-namespace Npgsql.GeoJSON.Internal;
-
-public class GeoJSONTypeHandlerResolver : TypeHandlerResolver
-{
- readonly NpgsqlDatabaseInfo _databaseInfo;
- readonly GeoJsonHandler? _geometryHandler, _geographyHandler;
- readonly bool _geographyAsDefault;
-
- static readonly ConcurrentDictionary CRSMaps = new();
-
- internal GeoJSONTypeHandlerResolver(NpgsqlConnector connector, GeoJSONOptions options, bool geographyAsDefault)
- {
- _databaseInfo = connector.DatabaseInfo;
- _geographyAsDefault = geographyAsDefault;
-
- var crsMap = (options & (GeoJSONOptions.ShortCRS | GeoJSONOptions.LongCRS)) == GeoJSONOptions.None
- ? default : CRSMaps.GetOrAdd(connector.Settings.ConnectionString, _ =>
- {
- var builder = new CrsMapBuilder();
- using var cmd = connector.CreateCommand(
- "SELECT min(srid), max(srid), auth_name " +
- "FROM(SELECT srid, auth_name, srid - rank() OVER(ORDER BY srid) AS range " +
- "FROM spatial_ref_sys) AS s GROUP BY range, auth_name ORDER BY 1;");
- cmd.AllResultTypesAreUnknown = true;
- using var reader = cmd.ExecuteReader();
-
- while (reader.Read())
- {
- builder.Add(new CrsMapEntry(
- int.Parse(reader.GetString(0)),
- int.Parse(reader.GetString(1)),
- reader.GetString(2)));
- }
-
- return builder.Build();
- });
-
- var (pgGeometryType, pgGeographyType) = (PgType("geometry"), PgType("geography"));
-
- if (pgGeometryType is not null)
- _geometryHandler = new GeoJsonHandler(pgGeometryType, options, crsMap);
- if (pgGeographyType is not null)
- _geographyHandler = new GeoJsonHandler(pgGeographyType, options, crsMap);
- }
-
- public override NpgsqlTypeHandler? ResolveByDataTypeName(string typeName)
- => typeName switch
- {
- "geometry" => _geometryHandler,
- "geography" => _geographyHandler,
- _ => null
- };
-
- public override NpgsqlTypeHandler? ResolveByClrType(Type type)
- => ClrTypeToDataTypeName(type, _geographyAsDefault) is { } dataTypeName && ResolveByDataTypeName(dataTypeName) is { } handler
- ? handler
- : null;
-
- internal static string? ClrTypeToDataTypeName(Type type, bool geographyAsDefault)
- => type.BaseType != typeof(GeoJSONObject)
- ? null
- : geographyAsDefault
- ? "geography"
- : "geometry";
-
- PostgresType? PgType(string pgTypeName) => _databaseInfo.TryGetPostgresTypeByName(pgTypeName, out var pgType) ? pgType : null;
-}
\ No newline at end of file
diff --git a/src/Npgsql.GeoJSON/Internal/GeoJSONTypeHandlerResolverFactory.cs b/src/Npgsql.GeoJSON/Internal/GeoJSONTypeHandlerResolverFactory.cs
deleted file mode 100644
index aae2c9102a..0000000000
--- a/src/Npgsql.GeoJSON/Internal/GeoJSONTypeHandlerResolverFactory.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System;
-using Npgsql.Internal;
-using Npgsql.Internal.TypeHandling;
-using Npgsql.Internal.TypeMapping;
-using Npgsql.TypeMapping;
-
-namespace Npgsql.GeoJSON.Internal;
-
-public class GeoJSONTypeHandlerResolverFactory : TypeHandlerResolverFactory
-{
- readonly GeoJSONOptions _options;
- readonly bool _geographyAsDefault;
-
- public GeoJSONTypeHandlerResolverFactory(GeoJSONOptions options, bool geographyAsDefault)
- => (_options, _geographyAsDefault) = (options, geographyAsDefault);
-
- public override TypeHandlerResolver Create(TypeMapper typeMapper, NpgsqlConnector connector)
- => new GeoJSONTypeHandlerResolver(connector, _options, _geographyAsDefault);
-
- public override TypeMappingResolver CreateMappingResolver() => new GeoJsonTypeMappingResolver(_geographyAsDefault);
-}
diff --git a/src/Npgsql.GeoJSON/Internal/GeoJSONTypeInfoResolver.cs b/src/Npgsql.GeoJSON/Internal/GeoJSONTypeInfoResolver.cs
new file mode 100644
index 0000000000..5ea3b8c9e3
--- /dev/null
+++ b/src/Npgsql.GeoJSON/Internal/GeoJSONTypeInfoResolver.cs
@@ -0,0 +1,76 @@
+using System;
+using GeoJSON.Net;
+using GeoJSON.Net.Geometry;
+using Npgsql.Internal;
+using Npgsql.Internal.Postgres;
+
+namespace Npgsql.GeoJSON.Internal;
+
+sealed class GeoJSONTypeInfoResolver : IPgTypeInfoResolver
+{
+ TypeInfoMappingCollection Mappings { get; }
+
+ internal GeoJSONTypeInfoResolver(GeoJSONOptions options, bool geographyAsDefault, CrsMap? crsMap = null)
+ {
+ Mappings = new TypeInfoMappingCollection();
+ AddInfos(Mappings, options, geographyAsDefault, crsMap);
+ // TODO opt-in arrays
+ AddArrayInfos(Mappings);
+ }
+
+ public PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options)
+ => Mappings.Find(type, dataTypeName, options);
+
+ static void AddInfos(TypeInfoMappingCollection mappings, GeoJSONOptions geoJsonOptions, bool geographyAsDefault, CrsMap? crsMap)
+ {
+ crsMap ??= new CrsMap(CrsMap.WellKnown);
+
+ var geometryMatchRequirement = !geographyAsDefault ? MatchRequirement.Single : MatchRequirement.DataTypeName;
+ var geographyMatchRequirement = geographyAsDefault ? MatchRequirement.Single : MatchRequirement.DataTypeName;
+
+ foreach (var dataTypeName in new[] { "geometry", "geography" })
+ {
+ var matchRequirement = dataTypeName == "geometry" ? geometryMatchRequirement : geographyMatchRequirement;
+
+ mappings.AddType(dataTypeName,
+ (options, mapping, _) => mapping.CreateInfo(options, new GeoJSONConverter(geoJsonOptions, crsMap)),
+ matchRequirement);
+ mappings.AddType(dataTypeName,
+ (options, mapping, _) => mapping.CreateInfo(options, new GeoJSONConverter(geoJsonOptions, crsMap)),
+ matchRequirement);
+ mappings.AddType(dataTypeName,
+ (options, mapping, _) => mapping.CreateInfo(options, new GeoJSONConverter(geoJsonOptions, crsMap)),
+ matchRequirement);
+ mappings.AddType(dataTypeName,
+ (options, mapping, _) => mapping.CreateInfo(options, new GeoJSONConverter(geoJsonOptions, crsMap)),
+ matchRequirement);
+ mappings.AddType(dataTypeName,
+ (options, mapping, _) => mapping.CreateInfo(options, new GeoJSONConverter(geoJsonOptions, crsMap)),
+ matchRequirement);
+ mappings.AddType(dataTypeName,
+ (options, mapping, _) => mapping.CreateInfo(options, new GeoJSONConverter(geoJsonOptions, crsMap)),
+ matchRequirement);
+ mappings.AddType(dataTypeName,
+ (options, mapping, _) => mapping.CreateInfo(options, new GeoJSONConverter(geoJsonOptions, crsMap)),
+ matchRequirement);
+ mappings.AddType(dataTypeName,
+ (options, mapping, _) => mapping.CreateInfo(options, new GeoJSONConverter(geoJsonOptions, crsMap)),
+ matchRequirement);
+ }
+ }
+
+ static void AddArrayInfos(TypeInfoMappingCollection mappings)
+ {
+ foreach (var dataTypeName in new[] { "geometry", "geography" })
+ {
+ mappings.AddArrayType(dataTypeName);
+ mappings.AddArrayType(dataTypeName);
+ mappings.AddArrayType(dataTypeName);
+ mappings.AddArrayType(dataTypeName);
+ mappings.AddArrayType(dataTypeName);
+ mappings.AddArrayType(dataTypeName);
+ mappings.AddArrayType(dataTypeName);
+ mappings.AddArrayType(dataTypeName);
+ }
+ }
+}
diff --git a/src/Npgsql.GeoJSON/Internal/GeoJsonTypeMappingResolver.cs b/src/Npgsql.GeoJSON/Internal/GeoJsonTypeMappingResolver.cs
deleted file mode 100644
index 137606538b..0000000000
--- a/src/Npgsql.GeoJSON/Internal/GeoJsonTypeMappingResolver.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using System;
-using Npgsql.Internal.TypeHandling;
-using Npgsql.Internal.TypeMapping;
-using Npgsql.PostgresTypes;
-using NpgsqlTypes;
-
-namespace Npgsql.GeoJSON.Internal;
-
-public class GeoJsonTypeMappingResolver : TypeMappingResolver
-{
- readonly bool _geographyAsDefault;
-
- public GeoJsonTypeMappingResolver(bool geographyAsDefault) => _geographyAsDefault = geographyAsDefault;
-
- public override string? GetDataTypeNameByClrType(Type type)
- => GeoJSONTypeHandlerResolver.ClrTypeToDataTypeName(type, _geographyAsDefault);
-
- public override TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName)
- => DoGetMappingByDataTypeName(dataTypeName);
-
- static TypeMappingInfo? DoGetMappingByDataTypeName(string dataTypeName)
- => dataTypeName switch
- {
- "geometry" => new(NpgsqlDbType.Geometry, "geometry"),
- "geography" => new(NpgsqlDbType.Geography, "geography"),
- _ => null
- };
-}
diff --git a/src/Npgsql.GeoJSON/NpgsqlGeoJSONExtensions.cs b/src/Npgsql.GeoJSON/NpgsqlGeoJSONExtensions.cs
index c59b0b21c7..6817094caa 100644
--- a/src/Npgsql.GeoJSON/NpgsqlGeoJSONExtensions.cs
+++ b/src/Npgsql.GeoJSON/NpgsqlGeoJSONExtensions.cs
@@ -1,4 +1,5 @@
-using Npgsql.GeoJSON.Internal;
+using Npgsql.GeoJSON;
+using Npgsql.GeoJSON.Internal;
using Npgsql.TypeMapping;
// ReSharper disable once CheckNamespace
@@ -17,7 +18,20 @@ public static class NpgsqlGeoJSONExtensions
/// Specifies that the geography type is used for mapping by default.
public static INpgsqlTypeMapper UseGeoJson(this INpgsqlTypeMapper mapper, GeoJSONOptions options = GeoJSONOptions.None, bool geographyAsDefault = false)
{
- mapper.AddTypeResolverFactory(new GeoJSONTypeHandlerResolverFactory(options, geographyAsDefault));
+ mapper.AddTypeInfoResolver(new GeoJSONTypeInfoResolver(options, geographyAsDefault, crsMap: null));
return mapper;
}
-}
\ No newline at end of file
+
+ ///
+ /// Sets up GeoJSON mappings for the PostGIS types.
+ ///
+ /// The type mapper to set up (global or connection-specific)
+ /// A custom crs map that might contain more or less entries than the default well-known crs map.
+ /// Options to use when constructing objects.
+ /// Specifies that the geography type is used for mapping by default.
+ public static INpgsqlTypeMapper UseGeoJson(this INpgsqlTypeMapper mapper, CrsMap crsMap, GeoJSONOptions options = GeoJSONOptions.None, bool geographyAsDefault = false)
+ {
+ mapper.AddTypeInfoResolver(new GeoJSONTypeInfoResolver(options, geographyAsDefault, crsMap));
+ return mapper;
+ }
+}
diff --git a/src/Npgsql.GeoJSON/PublicAPI.Unshipped.txt b/src/Npgsql.GeoJSON/PublicAPI.Unshipped.txt
index ab058de62d..be72efeb37 100644
--- a/src/Npgsql.GeoJSON/PublicAPI.Unshipped.txt
+++ b/src/Npgsql.GeoJSON/PublicAPI.Unshipped.txt
@@ -1 +1,5 @@
-#nullable enable
+Npgsql.GeoJSON.CrsMap
+Npgsql.GeoJSON.CrsMapExtensions
+static Npgsql.GeoJSON.CrsMapExtensions.GetCrsMap(this Npgsql.NpgsqlDataSource! dataSource) -> Npgsql.GeoJSON.CrsMap!
+static Npgsql.GeoJSON.CrsMapExtensions.GetCrsMapAsync(this Npgsql.NpgsqlDataSource! dataSource) -> System.Threading.Tasks.Task!
+static Npgsql.NpgsqlGeoJSONExtensions.UseGeoJson(this Npgsql.TypeMapping.INpgsqlTypeMapper! mapper, Npgsql.GeoJSON.CrsMap! crsMap, Npgsql.GeoJSONOptions options = Npgsql.GeoJSONOptions.None, bool geographyAsDefault = false) -> Npgsql.TypeMapping.INpgsqlTypeMapper!
\ No newline at end of file
diff --git a/src/Npgsql.Json.NET/Internal/JsonNetJsonConverter.cs b/src/Npgsql.Json.NET/Internal/JsonNetJsonConverter.cs
new file mode 100644
index 0000000000..42b7c88e0d
--- /dev/null
+++ b/src/Npgsql.Json.NET/Internal/JsonNetJsonConverter.cs
@@ -0,0 +1,121 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Npgsql.Internal;
+using JsonSerializer = Newtonsoft.Json.JsonSerializer;
+
+namespace Npgsql.Json.NET.Internal;
+
+sealed class JsonNetJsonConverter : PgStreamingConverter
+{
+ readonly bool _jsonb;
+ readonly Encoding _textEncoding;
+ readonly JsonSerializerSettings _settings;
+
+ public JsonNetJsonConverter(bool jsonb, Encoding textEncoding, JsonSerializerSettings settings)
+ {
+ _jsonb = jsonb;
+ _textEncoding = textEncoding;
+ _settings = settings;
+ }
+
+ public override T? Read(PgReader reader)
+ => (T?)JsonNetJsonConverter.Read(async: false, _jsonb, reader, typeof(T), _settings, _textEncoding, CancellationToken.None).GetAwaiter().GetResult();
+ public override async ValueTask ReadAsync(PgReader reader, CancellationToken cancellationToken = default)
+ => (T?)await JsonNetJsonConverter.Read(async: true, _jsonb, reader, typeof(T), _settings, _textEncoding, cancellationToken).ConfigureAwait(false);
+
+ public override Size GetSize(SizeContext context, T? value, ref object? writeState)
+ => JsonNetJsonConverter.GetSize(_jsonb, context, typeof(T), _settings, _textEncoding, value, ref writeState);
+
+ public override void Write(PgWriter writer, T? value)
+ => JsonNetJsonConverter.Write(_jsonb, async: false, writer, CancellationToken.None).GetAwaiter().GetResult();
+
+ public override ValueTask WriteAsync(PgWriter writer, T? value, CancellationToken cancellationToken = default)
+ => JsonNetJsonConverter.Write(_jsonb, async: true, writer, cancellationToken);
+}
+
+// Split out to avoid unneccesary code duplication.
+static class JsonNetJsonConverter
+{
+ public const byte JsonbProtocolVersion = 1;
+
+ public static async ValueTask