第09章 - 插件算子扩展
OpenGIS DAF 内置 9 个算子覆盖常见场景,但真实业务总有框架未预见的算子需求。为此,框架提供了基于 AssemblyLoadContext 的插件算子系统:开发者编写实现公共契约的 DLL,即可让自定义算子与内置算子在同一张 DAG 中无缝协作。本章结合演示插件 length_calculator,讲解插件开发、加载与常见坑。
9.1 插件系统概览
9.1.1 为什么需要插件宿主
CLI 提供了 daf operator import --dll <path> 命令,但它只在当前 CLI 进程内加载算子。而 daf run 每次都是新进程,导入的算子不会跨进程保留。
要让插件算子真正参与方案执行,需要同进程编程宿主:一个长期运行的 .NET 进程,加载插件 DLL 后执行方案。演示仓库提供了 OpenGisDAF.PluginHostDemo 作为参考宿主。
9.1.2 插件与内置算子无差别
在方案中,插件算子与内置算子没有任何区别——它们都实现同一个 IOperator 契约,通过 operatorId 被调度引擎识别,通过 upstream 绑定串联进 DAG。
9.2 演示插件:length_calculator
演示插件 OpenGisDAF.SamplePlugin 实现了一个 length_calculator 算子:计算线要素的长度(公里),写入 length_km 字段。
9.2.1 插件方案
plugin/plans/road-length.json 演示了内置算子与插件算子的协作:
{
"id": "demo-plugin-road-length",
"name": "道路长度统计(内置算子 + 插件算子协作)",
"version": "1.0.0",
"group": "demo-plugin",
"items": [
{
"id": "transform-roads",
"operatorId": "coordinate_transform",
"inputs": {
"source": {
"type": "external",
"sourceId": "data/roads.geojson"
}
},
"parameters": {
"source_epsg": 4326,
"target_epsg": 3857
},
"output": {
"adapterType": "console",
"isIntermediate": true
}
},
{
"id": "calc-road-length",
"operatorId": "length_calculator",
"inputs": {
"source": {
"type": "upstream",
"sourceId": "transform-roads"
}
},
"parameters": {
"target_field": "length_km"
},
"output": {
"adapterType": "geojson",
"targetPath": "output/plugin-demo/roads-with-length.geojson"
}
}
],
"executionPolicy": {
"failurePolicy": "stopOnAny"
}
}
注意:coordinate_transform(内置)先把道路从经纬度 4326 转到米制 3857,其输出通过 upstream 绑定流入 length_calculator(插件)。插件算子与内置算子在同一个 DAG 中无缝衔接。
9.2.2 运行插件方案
dotnet plugin/OpenGisDAF.PluginHostDemo/bin/Release/net10.0/OpenGisDAF.PluginHostDemo.dll \
--plugin <SamplePlugin.dll 路径> \
--plan plugin/plans/road-length.json
宿主进程加载插件 DLL 后执行方案,输出带 length_km 字段的道路 GeoJSON。
9.3 编写插件算子的三个关键点
9.3.1 最小依赖面
插件项目只引用 OpenGisDAF.Core 的公共契约(IOperator、IFeature、IFeatureSource 等),并自带 PluginFeature / PluginFeatureSource 等辅助类型,不引用实现层(Execution、Scheduling、Adapters 等)。这保证了插件与宿主解耦,宿主升级时插件无需重新编译。
9.3.2 不要复制宿主程序集(最常见坑)
这是插件开发最容易踩的坑。插件 csproj 引用 Core 时必须排除运行时复制:
<ProjectReference Include="..\..\src\OpenGisDAF.Core\OpenGisDAF.Core.csproj"
Private="false"
ExcludeAssets="runtime" />
如果 OpenGisDAF.Core.dll 被复制进了插件目录,PluginLoadContext 会加载插件自己的副本,导致 IOperator 接口与宿主加载的不是同一个 Type。结果是:插件算子被静默跳过,日志只出现 No IOperator implementations found,没有任何报错——排查起来非常困难。
9.3.3 Schema 可选
插件的输出源不实现 IFeatureSchemaProvider 也能正常工作。写出时框架会自动物化并推断字段并集、几何类型与 CRS。对于简单算子(如只新增一个计算字段),可以完全省略 schema 声明。
9.4 动手练习:编写 area_calculator
仿照 LengthCalculatorOperator,编写一个 area_calculator 算子,为面要素计算面积:
- 新建类实现
IOperator,OperatorId返回"area_calculator"; - 遍历输入要素,用
feature.Geometry.Area计算面积(假定输入已是米制坐标系,否则面积无意义); - 将结果写入
target_field参数指定的字段; - 编译后通过插件宿主加载,与
coordinate_transform串联验证。
9.5 本章小结
- 插件算子基于
AssemblyLoadContext动态加载,与内置算子在同一 DAG 中无差别协作; daf operator import只在当前进程生效,跨进程执行需使用同进程插件宿主;- 插件只引用
OpenGisDAF.Core公共契约,保持最小依赖面; - 不要复制宿主程序集到插件目录,否则
IOperator类型不一致导致算子被静默跳过; - 输出源 schema 可选,框架会自动物化推断。