第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 的公共契约IOperatorIFeatureIFeatureSource 等),并自带 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 算子,为面要素计算面积:

  1. 新建类实现 IOperatorOperatorId 返回 "area_calculator"
  2. 遍历输入要素,用 feature.Geometry.Area 计算面积(假定输入已是米制坐标系,否则面积无意义);
  3. 将结果写入 target_field 参数指定的字段;
  4. 编译后通过插件宿主加载,与 coordinate_transform 串联验证。

9.5 本章小结

  • 插件算子基于 AssemblyLoadContext 动态加载,与内置算子在同一 DAG 中无差别协作;
  • daf operator import 只在当前进程生效,跨进程执行需使用同进程插件宿主;
  • 插件只引用 OpenGisDAF.Core 公共契约,保持最小依赖面;
  • 不要复制宿主程序集到插件目录,否则 IOperator 类型不一致导致算子被静默跳过;
  • 输出源 schema 可选,框架会自动物化推断。

上一章:第08章 失败策略与调度 目录 下一章:第10章 方案管理与最佳实践