一、先把问题说清楚
ℹ️ 读者定位
适合你,如果: 你会写 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_Intersects、AsText 或 spatialite_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 项目,完整走一遍:
- 准备 NuGet 和原生 DLL。
- 打开 SQLite 并加载 SpatiaLite。
- 用 NetTopologySuite 把 Shapefile 导入空间表。
- 用一个独立 C# 类注册 SQL UDF。
- 在 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_columns、spatial_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;
成功时应该看到 POINT、LINESTRING 或 POLYGON,以及对应的 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 原生目录和数据库文件。实际操作按这个顺序:
- 选择
.shp文件。 - 确认同目录存在同名
.shx和.dbf。 - 确认原生目录指向
mod_spatialite.dll所在目录。 - 点击"检查 Shapefile",查看 DotSpatial 与 NTS 的要素数量。
- 点击"读取并导入 SpatiaLite"。
- 在 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_upper、gis_area 和 gis_buffer 都能从 SQL 调用。
七、把坑提前说清楚
7.1 常见故障
| 现象 | 优先检查 |
|---|---|
Unable to load DLL |
程序位数、原生目录位数、依赖 DLL 和 PATH |
no such function: spatialite_version |
是否执行 EnableExtensions 和 LoadExtension |
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。
参考资料: