C# 接入 SpatiaLite——从原生 DLL 加载到 Shapefile 入库与自定义 SQL

一、先把问题说清楚

ℹ️ 读者定位

适合你,如果: 你会写 C#,正在做 WinForms、WPF 或本地 GIS 工具,需要读取 Shapefile、执行空间 SQL,或者把自己的业务方法注册成 SQL 函数。

开始前需要: .NET 8 SDK、Windows 开发环境、x64 运行条件,以及一套可用的 SpatiaLite 原生 DLL。

读完可以完成: 建立 C# → SQLite → SpatiaLite 的连接,导入 Shapefile,并用 CreateFunction 注册 C# UDF。

暂时不适合: 跨平台发布、云端多用户并发或完整 GIS 服务治理场景;本文聚焦 Windows 本地嵌入式应用。

在工作中,我们经常会遇到一个很容易误判的问题:C# 程序明明已经能打开 SQLite 文件,但执行 ST_IntersectsAsTextspatialite_version() 时,却提示找不到函数。

这往往不是 SQL 写错了,而是 SpatiaLite 还没有真正进入当前 SQLite 连接。SQLite 是数据库内核,空间能力来自 mod_spatialite.dll;而这个扩展又依赖匹配的 SQLite、GEOS、PROJ 和 RTTOPO 等原生库。

所以,C# 接入 SpatiaLite 的关键不是"装一个包",而是把原生依赖链接完整。

bash 复制代码
C# 托管连接
  ↓
指定的 libsqlite3-0.dll
  ↓
加载 mod_spatialite.dll
  ↓
SpatiaLite 空间函数和元数据
  ↓
NTS 处理 Shapefile 与 WKB

C

本文使用一个已经跑通的 。NET 8 WinForms 项目,完整走一遍:

  1. 准备 NuGet 和原生 DLL。
  2. 打开 SQLite 并加载 SpatiaLite。
  3. 用 NetTopologySuite 把 Shapefile 导入空间表。
  4. 用一个独立 C# 类注册 SQL UDF。
  5. 在 WinForms 中执行查询并验证结果。

二、先让依赖链跑起来

2.1 项目依赖各自负责什么

项目目标是 net8.0-windows,运行目标固定为 win-x64。核心依赖如下:

bash 复制代码
<ItemGroup>
  <PackageReference Include="DotSpatial.Data" Version="4.0.656" />
  <PackageReference Include="NetTopologySuite.IO.Esri.Shapefile" Version="1.2.0" />
  <PackageReference Include="Microsoft.Data.Sqlite.Core" Version="8.0.22" />
  <PackageReference Include="SQLitePCLRaw.core" Version="2.1.10" />
  <PackageReference Include="SQLitePCLRaw.provider.dynamic_cdecl" Version="2.1.10" />
  <PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" />
</ItemGroup>

不要把这些库理解成同一种东西:

组件 职责
DotSpatial.Data 检查 Shapefile 图层、属性表和要素数量
NetTopologySuite.IO.Esri.Shapefile 读取 Shapefile 几何与属性
NetTopologySuite 处理几何、WKB、面积和缓冲区
Microsoft.Data.Sqlite.Core 提供 C# SQLite 连接和命令 API
SQLitePCLRaw 动态 provider 让托管连接使用指定的 SQLite 原生 DLL
mod_spatialite.dll 提供空间函数、空间元数据和空间类型

这里特意使用 Microsoft.Data.Sqlite.Core,因为我们要自己指定 SQLite 原生库。Microsoft 官方把这类场景称为 custom SQLite version,并建议由应用负责部署自己的 native library。自定义 SQLite 版本

2.2 原生目录必须完整

示例使用的 SpatiaLite 目录:

bash 复制代码
D:\Code\Java\gis\shp2spatialite\native\mod_spatialite-5.1.0-win-amd64

至少要确认下面这些文件存在:

bash 复制代码
libsqlite3-0.dll
mod_spatialite.dll
libgeos_c.dll
libproj_9_2.dll
librttopo-1.dll
proj.db

不要只复制 mod_spatialite.dll 扩展加载时还要解析依赖库;缺少其中任意一个,最终都可能表现为扩展加载失败。

2.3 x64 必须对齐

原生目录名称是 win-amd64,所以项目也要固定 x64:

bash 复制代码
<PropertyGroup>
  <PlatformTarget>x64</PlatformTarget>
  <RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>

Any CPU 或 x86 很容易出现"编译成功、运行加载 DLL 失败"。第一次排查时,先检查程序位数和原生目录位数。

三、打开连接,再加载空间扩展

3.1 先配置原生 SQLite

项目用 SpatialiteNative 集中处理原生环境:

bash 复制代码
public static void Initialize(string nativeDirectory)
{
    var directory = Path.GetFullPath(nativeDirectory);
    var sqlitePath = Path.Combine(directory, "libsqlite3-0.dll");
    var extensionPath = Path.Combine(directory, "mod_spatialite.dll");
 
    if (!File.Exists(sqlitePath))
        throw new FileNotFoundException("未找到 libsqlite3-0.dll", sqlitePath);
 
    if (!File.Exists(extensionPath))
        throw new FileNotFoundException("未找到 mod_spatialite.dll", extensionPath);
 
    SetDllDirectory(directory);
 
    var provider = new SQLitePCL.SQLite3Provider_dynamic_cdecl();
    SQLitePCL.SQLite3Provider_dynamic_cdecl.Setup(
        "sqlite3",
        new NativeLibraryAdapter(sqlitePath));
    SQLitePCL.raw.SetProvider(provider);
}

完整实现还会把目录加入当前进程的 PATH,并用 NativeLibrary.Load / TryGetExport 查找 SQLite 函数。

顺序很重要:先配置 provider,再创建第一个 SqliteConnection。不要连接已经打开后才切换 SQLite 原生库。

3.2 加载 mod_spatialite.dll

数据库服务中的关键代码:

bash 复制代码
SpatialiteNative.Initialize(_nativeDirectory);
 
var connectionString = new SqliteConnectionStringBuilder
{
    DataSource = _databasePath,
    Mode = SqliteOpenMode.ReadWriteCreate,
    Cache = SqliteCacheMode.Shared
}.ToString();
 
_connection = new SqliteConnection(connectionString);
_connection.Open();
_connection.EnableExtensions(true);
_connection.LoadExtension(
    Path.Combine(_nativeDirectory, "mod_spatialite.dll"),
    "sqlite3_modspatialite_init");

EnableExtensions(true) 只是允许扩展加载,LoadExtension 才是把 SpatiaLite 注入当前连接。官方扩展示例也是先打开连接,再调用 LoadExtension加载 SQLite 扩展

3.3 立即做三层验证

连接打开后,不要直接执行复杂空间分析,先做最小验证:

bash 复制代码
SELECT sqlite_version();
SELECT spatialite_version();
SELECT GeometryType(GeomFromText('POINT(116.391 39.907)', 4326));

三条 SQL 分别验证 SQLite、SpatiaLite 和几何构造。任何一层失败,都能快速定位问题。

接着初始化空间元数据:

bash 复制代码
CustomSqlFunctions.Register(_connection);
ExecuteNonQuery("SELECT InitSpatialMetaData(1);");

InitSpatialMetaData(1) 会创建 geometry_columnsspatial_ref_sys 等基础表,但不会替你创建业务表。

02-winforms-spatialite-connected

四、把 Shapefile 变成空间表

4.1 DotSpatial 和 NTS 的分工

项目同时使用 DotSpatial 和 NetTopologySuite,但并不是让两个库重复读取:

  • DotSpatial 用于检查图层、属性列和要素数量。
  • NTS 用于读取几何、写出 WKB,并执行 C# UDF 中的几何计算。

Shapefile 也不是单文件格式,通常需要:

bash 复制代码
roads.shp
roads.shx
roads.dbf

如果带坐标系信息,还应有:

bash 复制代码
roads.prj

4.2 创建普通属性列和空间列

导入流程先根据 DBF 字段创建属性列:

bash 复制代码
CREATE TABLE "features" (
    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
    "name" TEXT,
    "rank" INTEGER
);

再由 SpatiaLite 增加空间列:

bash 复制代码
SELECT AddGeometryColumn(
    'features', 'geom', 0, 'GEOMETRY', 2
);

示例默认传入 SRID 0,只表示当前没有确认坐标系。若 .prj 或业务配置明确是 EPSG:4326,应传入 4326。不要把 SRID 0 当成 WGS84。

4.3 用 WKB 连接 NTS 和 SpatiaLite

NTS 侧写 WKB:

bash 复制代码
var writer = new WKBWriter();
geometryParameter.Value = writer.Write(feature.Geometry);

数据库侧用 GeomFromWKB 构造几何:

bash 复制代码
INSERT INTO "features" ("name", "geom")
VALUES ($name, GeomFromWKB($geom, $srid));

WKB 是这一层最稳定的接缝:C# 不需要拼 WKT,几何通过参数化 BLOB 传输,最终仍然落成 SpatiaLite 的空间列。

导入后先执行:

bash 复制代码
SELECT
    id,
    GeometryType(geom) AS geometry_type,
    AsText(geom) AS wkt
FROM features
LIMIT 100;

成功时应该看到 POINTLINESTRINGPOLYGON,以及对应的 WKT。

02-winforms-spatialite-connected

五、用独立类实现 C# UDF

5.1 把注册逻辑集中起来

不要把 UDF 注册代码放在按钮事件里。项目用单独的 CustomSqlFunctions 类:

bash 复制代码
using Microsoft.Data.Sqlite;
using NetTopologySuite.IO;
 
internal static class CustomSqlFunctions
{
    public static void Register(SqliteConnection connection)
    {
        ArgumentNullException.ThrowIfNull(connection);
 
        connection.CreateFunction<string?, string?>(
            "gis_upper",
            value => value?.Trim().ToUpperInvariant(),
            isDeterministic: true);
 
        connection.CreateFunction<byte[], double>(
            "gis_area",
            wkb => new WKBReader().Read(wkb).Area,
            isDeterministic: true);
 
        connection.CreateFunction<byte[], double, byte[]>(
            "gis_buffer",
            (wkb, distance) =>
            {
                var geometry = new WKBReader().Read(wkb);
                return new WKBWriter().Write(geometry.Buffer(distance));
            },
            isDeterministic: true);
    }
}

然后在 mod_spatialite.dll 加载成功后调用:

bash 复制代码
CustomSqlFunctions.Register(_connection);

这就是 SQLite 场景下的 C# 版 Java UDF:C# 方法运行在应用进程里,SQL 负责按行调用。除了标量函数,Microsoft.Data.Sqlite 还提供聚合函数注册能力。用户自定义函数

5.2 文本函数和面积函数

bash 复制代码
SELECT
    id,
    gis_upper(name) AS normalized_name,
    gis_area(ST_AsBinary(geom)) AS area
FROM features;

面积函数内部的路径是:

bash 复制代码
SpatiaLite geom
  → ST_AsBinary
  → byte[] WKB
  → C# WKBReader
  → NTS Geometry.Area

注意面积单位:如果输入是经纬度坐标,结果是平面计算意义下的坐标单位平方,不能直接当作平方米。

5.3 返回几何时先返回 WKB

gis_buffer 返回 WKB,SQL 再把它转回几何:

bash 复制代码
SELECT
    id,
    AsText(
        GeomFromWKB(
            gis_buffer(ST_AsBinary(geom), 10),
            0
        )
    ) AS buffered_wkt
FROM features;

这比尝试让 C# 直接返回 SpatiaLite Geometry 更容易维护:C# 与 SQLite 之间只约定 byte[],空间类型的构造仍然由 SpatiaLite 完成。

⚠️ UDF 不会保存进数据库

函数注册在当前 SqliteConnection 上,不会写入 .sqlite 文件。每次新建连接都必须重新调用 CustomSqlFunctions.Register(connection);多个连接也要分别注册。

六、在 WinForms 中验证完整链路

6.1 操作顺序

项目主窗体提供三个路径输入:Shapefile、SpatiaLite 原生目录和数据库文件。实际操作按这个顺序:

  1. 选择 .shp 文件。
  2. 确认同目录存在同名 .shx.dbf
  3. 确认原生目录指向 mod_spatialite.dll 所在目录。
  4. 点击"检查 Shapefile",查看 DotSpatial 与 NTS 的要素数量。
  5. 点击"读取并导入 SpatiaLite"。
  6. 在 SQL 文本框执行普通空间 SQL 或 C# UDF。

查询结果绑定到 DataGridView,日志区域记录版本、导入数量和查询行数。

6.2 推荐验收顺序

bash 复制代码
SELECT sqlite_version();
SELECT spatialite_version();
SELECT GeometryType(GeomFromText('POINT(116.391 39.907)', 4326));
SELECT gis_upper(' test ');
SELECT Count(*) FROM features;
SELECT id, GeometryType(geom), AsText(geom) FROM features LIMIT 10;

每条 SQL 只验证一层:SQLite、SpatiaLite、几何构造、C# UDF、业务表和真实几何数据。

✅ 已验证

示例项目已经用 SpatiaLite 5.1.0 原生目录编译并运行自测:临时点 Shapefile 成功导入,gis_uppergis_areagis_buffer 都能从 SQL 调用。

七、把坑提前说清楚

7.1 常见故障

现象 优先检查
Unable to load DLL 程序位数、原生目录位数、依赖 DLL 和 PATH
no such function: spatialite_version 是否执行 EnableExtensionsLoadExtension
no such function: gis_area 是否对当前连接调用了 CustomSqlFunctions.Register
中文 DBF 乱码 CodePagesEncodingProvider 和 DBF 实际编码
面积不对 SRID、坐标单位和投影坐标系
重启后 UDF 消失 正常现象,UDF 需要随连接重新注册

7.2 这个方案适合什么场景

场景 建议
Windows 本地 GIS 工具 推荐,部署清晰,数据库可以是单文件
Shapefile 批量导入 推荐,NTS 和 SpatiaLite 分工明确
少量业务计算函数 推荐,直接使用 CreateFunction
虚拟表、表值函数或极致性能 考虑 C/C++ 原生 SQLite 扩展
多用户在线 GIS 服务 不要直接照搬 WinForms 结构,应重新设计服务层和并发模型

最后记住一句话:

bash 复制代码
先让同一套 SQLite 原生库和 SpatiaLite 扩展加载成功,
再处理 WKB 和空间列,最后注册 C# UDF。

参考资料:

相关推荐
自己的九又四分之三站台12 小时前
RasterLite2:把 SQLite 栅格覆盖、元数据和地图配置拆开讲清楚
地理信息
梦想的初衷~3 个月前
AI辅助下基于ArcGIS Pro的SWAT模型全流程高效建模实践与深度进阶应用
人工智能·arcgis·气候·水文·地理信息·环境科学
众智鸿图6 个月前
解锁AR“透视眼”丨众智鸿图助力广州水投实现AR智能巡检新跨越
人工智能·ar·地理信息·智慧水务·城市基础设施智能化·管网管理
zhz52147 个月前
ArcGIS实习教程
arcgis·地理信息·空间数据
铉铉这波能秀7 个月前
如何在arcmap中将shp等文件类型导出为表格(四种方法)
数据库·arcgis·数据分析·arcmap·地理信息·shp
自己的九又四分之三站台8 个月前
maputnik项目实操
地理信息
搞科研的小刘选手9 个月前
【同济大学主办】第十一届能源资源与环境工程研究进展国际学术会议(ICAESEE 2025)
大数据·人工智能·能源·材质·材料工程·地理信息
搞科研的小刘选手9 个月前
【罗马第三大学主办 | 可线上参会】第四届地理信息与遥感技术国际学术会议(GIRST 2025)
遥感·地理信息·测绘·地理信息系统·卫星导航·测量与测绘·地图制图
GIS小小研究僧1 年前
ArcGIS Pro+PS 实现地形渲染效果图
arcgis·gis·qgis·地理信息