Gdal.Npgsql
8.2.0
dotnet add package Gdal.Npgsql --version 8.2.0
NuGet\Install-Package Gdal.Npgsql -Version 8.2.0
<PackageReference Include="Gdal.Npgsql" Version="8.2.0" />
<PackageVersion Include="Gdal.Npgsql" Version="8.2.0" />
<PackageReference Include="Gdal.Npgsql" />
paket add Gdal.Npgsql --version 8.2.0
#r "nuget: Gdal.Npgsql, 8.2.0"
#:package Gdal.Npgsql@8.2.0
#addin nuget:?package=Gdal.Npgsql&version=8.2.0
#tool nuget:?package=Gdal.Npgsql&version=8.2.0
The package is an Npgsql plugin that allows you to interact with spatial data provided by the PostgreSQL PostGIS extension. On the .NET side, the plugin adds support for geometry types from the GDAL(OGR) library, allowing you to read and write them directly to PostgreSQL.
GDAL is connected via the nuget package MaxRev.Gdal.Core
Usage
Add a dependency on this package, then register the plugin on an NpgsqlDataSource. PostGIS
geometry and geography columns — and arrays of them — are mapped to PgGeometry.
using Npgsql;
using Npgsql.Gdal;
using OSGeo.OGR;
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
dataSourceBuilder.UseGdal();
var dataSource = dataSourceBuilder.Build();
using (var conn = await dataSource.OpenConnectionAsync())
{
using var point = Geometry.CreateFromWkt("POINT (30 10)");
using (var cmd = new NpgsqlCommand("INSERT INTO data (geom) VALUES (@p)", conn))
{
cmd.Parameters.AddWithValue("@p", PgGeometry.FromGeometry(point, srid: 4326));
cmd.ExecuteNonQuery();
}
using (var cmd = new NpgsqlCommand("SELECT geom FROM data", conn))
using (var reader = cmd.ExecuteReader())
{
reader.Read();
var value = reader.GetFieldValue<PgGeometry>(0);
Console.WriteLine(value.GeometryType); // no OGR object is created
Console.WriteLine(value.SRID);
using var geometry = value.CreateGeometry(); // you own it, you dispose it
}
}
Creating a geometry, and getting the bytes back
PgGeometry always holds the geometry as EWKB internally, whichever way it was built. These
members cover the conversions:
| in | out | |
|---|---|---|
| WKB, SRID taken from the bytes | FromWkb(byte[] wkb) |
GetWkb() — without the SRID |
| WKB, SRID stated by the caller | FromWkb(byte[] wkb, int srid) |
|
| EWKB, SRID carried by the bytes | FromEWkb(byte[] ewkb) |
GetEWkb() — with the SRID |
| OGR geometry | FromGeometry(Geometry g, int srid = 0) |
CreateGeometry() |
All of the inputs accept either encoding of Z and M — the ISO form (+1000/+2000/+3000 on the
geometry type) and the flag form PostGIS uses — and either byte order. Whatever comes in is
converted to EWKB right away, so the encoding of an existing PgGeometry never depends on how it
was made. Byte order is preserved.
The one-argument FromWkb takes the SRID from the bytes and settles on 0 when they carry none, so
it accepts anything without complaint; FromEWkb is the same operation under a name that says what
the bytes are.
Naming an SRID in the second argument is an assertion about the bytes, not an override. Bytes that
carry no SRID get the one you name; bytes that carry a different one raise ArgumentException
rather than silently picking a winner. That applies to 0 as well — FromWkb(bytes, 0) asserts the
geometry has no SRID, which is why "don't care" is a separate overload rather than a default value.
var fromDatabaseBytes = PgGeometry.FromWkb(ewkb); // SRID: whatever the bytes say
var withKnownSrid = PgGeometry.FromWkb(wkb, 4326); // attaches 4326, or throws on a clash
byte[] ewkbOut = withKnownSrid.GetEWkb(); // for PostGIS: carries the SRID
byte[] plainOut = withKnownSrid.GetWkb(); // for everything else: no SRID
int srid = withKnownSrid.SRID;
GetWkb() and GetEWkb() both return a fresh array every time, so the caller is free to keep or
modify what they get.
Bulk insert
GetEWkb() is what a bulk insert needs when the geometries do not go through Npgsql's own type
mapping — for example when they are copied into a bytea staging column and turned into geometries
on the server:
using (var writer = await conn.BeginBinaryImportAsync(
"COPY staging (id, ewkb) FROM STDIN (FORMAT BINARY)"))
{
foreach (var (id, geometry) in rows)
{
await writer.StartRowAsync();
await writer.WriteAsync(id);
await writer.WriteAsync(PgGeometry.FromGeometry(geometry, srid: 4326).GetEWkb());
}
await writer.CompleteAsync();
}
// INSERT INTO data (id, geom) SELECT id, ST_GeomFromEWKB(ewkb) FROM staging;
When the target column is a geometry and the plugin is registered, a PgGeometry can also be
written to the COPY stream directly, without touching the bytes at all.
Why PgGeometry and not Geometry
OSGeo.OGR.Geometry wraps native memory and has to be disposed. If a query result were mapped
straight to it, every geometry read from the database would be created with no owner — including
the ones your code never looks at. Their memory is invisible to the .NET garbage collector, so it
is released late and unpredictably.
PgGeometry holds the geometry as EWKB bytes instead:
- reading a row costs a buffer copy, and no native object is created at all;
CreateGeometry()builds the OGR object only when you ask for one, and the caller owns it;FromGeometry(geometry, srid)serialises immediately — the geometry you pass stays yours;GetEWkb()hands you the PostGIS wire encoding when you need to feed the bytes to the server yourself, andGetWkb()the same geometry without the SRID;- the type is immutable, so it is safe to cache, store and pass between threads;
- the database path never touches native GDAL, so reading and writing works without
GdalBase.ConfigureAll(). OnlyCreateGeometry()requires the native libraries.
PgGeometry accepts both the ISO and the PostGIS encoding of Z and M on input, and preserves
whatever byte order was used.
Migration from 8.0.x
Version 8.2.0 is a breaking change.
| 8.0.x | 8.1.0 |
|---|---|
UseGdal() mapping to OSGeo.OGR.Geometry |
removed — use UseGdal() with PgGeometry |
UseGdalExt() |
removed — UseGdal() now registers PgGeometry |
ExtGeometry |
PgGeometry |
ExtGeometry.Create(geometry, srid) |
PgGeometry.FromGeometry(geometry, srid) |
extGeometry.Geometry |
pgGeometry.CreateGeometry() — returns a new object you must dispose |
ExtGeometry.SRID (int?, null when unset) |
PgGeometry.SRID (int, 0 when unset) |
| — | PgGeometry.FromWkb(wkb[, srid]) / FromEWkb(ewkb) / GetWkb() / GetEWkb() |
Reading a column now looks like this:
// 8.0.x
var eg = (reader[0] as ExtGeometry)!;
DoSomething(eg.Geometry); // never disposed
// 8.1.0
var value = reader.GetFieldValue<PgGeometry>(0);
using var geometry = value.CreateGeometry();
DoSomething(geometry);
Fixed along the way:
Mvalues ofPOINTM/LINESTRINGMgeometries were written as0;- the SRID of a geometry was read from the database but never written back to it;
- array mappings (
geometry[],geography[]) were registered but never took effect; - rings and sub-geometries were cloned and leaked while reading collections.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- MaxRev.Gdal.Core (>= 3.10.0.306)
- Npgsql (>= 8.0.3 && < 9.0.0)
- System.Memory (>= 4.5.5)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
8.2.0 is a breaking change. ExtGeometry and the direct mapping to OSGeo.OGR.Geometry are replaced by the immutable PgGeometry, which holds the geometry as EWKB bytes and creates an OGR object only when explicitly asked. Conversions go through FromWkb/FromEWkb/FromGeometry on the way in and GetWkb/GetEWkb/CreateGeometry on the way out. Fixes M values being written as zero, the SRID never being written back to the database, and array mappings never taking effect. See the Migration section in the README.