- Core - Architecture - Snet Docs

Snet 框架核心架构

源码路径: Snet.Core/ 命名空间根: Snet.Core


概述

Snet 框架是一个统一的工业数据平台,构建于单一基类 CoreUnify<O, D> 之上。所有操作组件(PLC 驱动、通信客户端、消息队列、缓存、插件、虚拟地址处理器)都继承自此根基类。该框架提供了一致的单例生命周期、结构化事件系统、内置计时/日志记录、多语言支持以及跨所有子系统的标准化 OperateResult 契约。


架构图

                         +-------------------------------+
                         |       CoreUnify<O, D>        |
                         |  (extend/CoreUnify.cs)        |
                         |                               |
                         |  单例池 (最大 255 实例)         |
                         |  3 对事件 (同步+异步)          |
                         |  计时 (Beg/EndOperate)        |
                         |  Serilog 日志                 |
                         |  多语言 (zh/en)                |
                         |  反射参数                       |
                         +-------------+-----------------+
                                       |
         +-----------------+----------+--------+----------+----------------+
         |                 |                   |          |                |
   DaqAbstract    CommunicationAbstract  MqAbstract  CommunicationService  Direct
   (IDaq)         (ICommunication)       (IMq)       Abstract              Subclasses
         |                 |                   |    (ICommunicationService)   |
   +-----+-----+    +------+------+     +-----+-----+   +----+----+    +---+---+
   | 33 种驱动  |    | SerialOperate|    | MqttClient |   |TcpService|   |Subscribe|
   | Snet.DB    |    | TcpClient    |    | Kafka      |   |WsService |   |Channel  |
   | Snet.Sim   |    | WsClient     |    | RabbitMQ   |   |UdpService|   |MqOperate|
   | Snet.TEP   |    | UdpClient    |    | NetMQ      |                 |HttpClient|
   | Snet.Opc   |    | UdpMulticast |    | NettyClient|                 |HttpService|
                     | UdpBroadcast |                                   |Reflection |
                     +--------------+                                   |VirtualAddr|
                                                                        |BytesHandler|
                                                                        |ShareCache |
                                                                        |ProcessCache|
                                                                        |Plugin     |
                                                                        +-----------+

1. CoreUnify <O, D> -- 万法之基

文件: Snet.Core/extend/CoreUnify.cs 命名空间: Snet.Core.extend 实现接口: IGetParam, ICreateInstance, IEvent, ILog, ILanguage, IDisposable, IAsyncDisposable 类型约束: where O : class, where D : class

CoreUnify 是框架中所有操作组件的根基类。O 是操作者类型(正在定义的具体类),D 是配置/基础数据类型,用作单例字典的键。

1.1 单例池

protected static readonly ConcurrentDictionary<D, O> objList = new ConcurrentDictionary<D, O>();
protected static readonly int maxInstanceCount = byte.MaxValue; // 255

每个派生类共享一个静态字典,将配置对象(D)映射到其单例操作者实例(O)。池的上限为 255 个实例。实例创建由 SemaphoreSlim(1, 1) 保护,防止竞争条件。

1.2 实例工厂

// 异步 -- 推荐使用
public static async Task<O> InstanceAsync(D? param = null, CancellationToken token = default)

// 同步便捷包装
public static O Instance(D? param = null)

工厂算法:

  1. 如果 param 为 null,通过 Activator.CreateInstance 创建默认 D
  2. 获取信号量。
  3. objList 中精确匹配键。
  4. 如果未找到,通过对所有现有键执行 Comparer(param).result 进行深度比较查找。
  5. 如果仍未找到匹配,调用私有方法 CreateInstance(param),使用反射调用构造函数 typeof(O).GetConstructor([typeof(D)])
  6. 通过 TryAdd 将新实例添加到 objList

使用示例:

// 创建或获取 Modbus RTU 驱动实例
var basics = new ModbusData
{
    IP = "192.168.1.100",
    Port = 502,
    StationNo = 1
};
var modbus = await ModbusOperate.InstanceAsync(basics);

// 后续使用等效数据的调用返回的是同一个实例
var sameModbus = await ModbusOperate.InstanceAsync(basics);
// modbus == sameModbus  (同一个对象引用)

// 打开连接、读取数据、关闭连接
await modbus.OnAsync();
var result = await modbus.ReadAsync(new Address
{
    SN = Guid.NewGuid().ToString(),
    AddressArray = new List<AddressDetails>
    {
        new AddressDetails { AddressName = "D100", AddressDataType = DataType.Int }
    }
});
await modbus.OffAsync();

1.3 计时 -- BegOperate / EndOperate

每个方法都可以用计时调用来封装其执行过程,生成标准化的 OperateResult

// 方法开始 -- 记录开始时间戳
string methodName = await BegOperateAsync(token);

// 方法结束 -- 计算耗时(毫秒),格式化本地化日志消息
return await EndOperateAsync(
    status: true,
    message: "操作完成",
    resultData: someResult,
    token: token
);

EndOperate 会自动:

  • 通过 TimeHandler.StopRecord() 计算耗时(毫秒)。
  • 格式化包含组件标签、方法名、成功/失败状态、源文件和行号的日志消息。
  • 如果 status == falselogOutput == true,通过 LogHelper.ErrorAsync(Serilog)写入错误条目。
  • 返回 OperateResult(status, message, runTime, resultData)

1.4 事件 -- 三对同步/异步事件

同步事件 异步事件包装器 事件参数类型
OnDataEvent OnDataEventAsync(通过 EventingWrapperAsync EventDataResult
OnInfoEvent OnInfoEventAsync(通过 EventingWrapperAsync EventInfoResult
OnLanguageEvent OnLanguageEventAsync(通过 EventingWrapperAsync EventLanguageResult

每个异步事件使用私有的 EventingWrapperAsync<T> 实例,提供 AddHandler/RemoveHandler 方法。受保护的 OnDataEventHandler / OnInfoEventHandler / OnLanguageEventHandler 方法同时触发同步和异步订阅者。语言事件在构造函数中自动注册,在 Dispose/DisposeAsync 中注销,使用缓存的委托实例以防止内存泄漏。

订阅事件:

var modbus = await ModbusOperate.InstanceAsync(basics);

// 同步处理程序
modbus.OnDataEvent += (sender, e) =>
{
    Console.WriteLine($"收到数据: {e.GetSource<SomeDataType>()}");
};

// 异步处理程序(安全即发即忘)
modbus.OnDataEventAsync += async (sender, e) =>
{
    var data = e.GetSource<SomeDataType>();
    await SaveToDatabaseAsync(data);
};

1.5 日志 -- Serilog 集成

LogOperateSet / LogOperateGet 控制底层 LogHelper 使用的 LogModel 配置:

// 配置日志及通知回调
await modbus.LogOperateSetAsync(
    logOut: true,
    consoleOut: true,
    notice: (source, level, message, ex) =>
    {
        // 自定义通知逻辑
    }
);

同时支持同步 Action<...> 和异步 Func<..., Task> 通知回调。

1.6 多语言 -- ILanguage

// 获取翻译值
string translated = modbus.GetLanguageValue("openSuccess");

// 运行时切换语言
modbus.SetLanguage(LanguageType.en);  // 英文
modbus.SetLanguage(LanguageType.zh);  // 中文

// 异步变体
string translated = await modbus.GetLanguageValueAsync("openSuccess", token: token);

语言值通过 LanguageHandler 静态委托系统解析。切换语言会在所有已注册实例上触发 OnLanguageEvent/OnLanguageEventAsync

1.7 参数自省 -- IGetParam

GetParam() 使用反射生成适用于动态 UI 表单生成的参数树:

var result = await modbus.GetParamAsync();
// result.ResultData 包含一个 ParamModel,其中有所有可配置属性

// 原始基础数据
var rawResult = await modbus.GetParamAsync(getBasicsParam: true);

// 自动分配参数(遵循 AutoAllocatingTagAttribute 特性)
var autoParam = await modbus.GetAutoAllocatingParamAsync();

1.8 构造函数链

// 无参:调用 RegisterLanguageEvent()
protected CoreUnify()

// 有参:存储 basics,然后 RegisterLanguageEvent()
protected CoreUnify(D param)

1.9 资源清理

// Dispose 从单例字典中移除并注销语言事件
public virtual void Dispose()
{
    UnregisterLanguageEvent();
    objList?.Remove(basics, out _);
    GC.SuppressFinalize(this);
}

2. DaqAbstract <O, D> -- 数据采集基类

文件: Snet.Core/abstract/DaqAbstract.cs 继承: CoreUnify<O, D> 实现: IDaq

所有数据采集驱动程序(PLC、OPC、数据库、仿真)的基类。它扩展了 CoreUnify,增加了 WebApi 远程控制能力和类型安全的写入重载。静态构造函数注册 CodePagesEncodingProvider 以支持 GB2312 编码。

2.1 抽象方法(具体驱动程序必须实现)

方法 签名 用途
OnAsync Task<OperateResult> OnAsync(CancellationToken) 激活/打开设备连接
OffAsync Task<OperateResult> OffAsync(bool hardClose, CancellationToken) 停用/关闭,支持可选的硬关闭
ReadAsync Task<OperateResult> ReadAsync(Address, CancellationToken) 从指定地址读取数值
WriteAsync Task<OperateResult> WriteAsync(ConcurrentDictionary<string, (object, EncodingType?)>, CancellationToken) 将类型化数值写入地址
SubscribeAsync Task<OperateResult> SubscribeAsync(Address, CancellationToken) 启动周期性轮询订阅
UnSubscribeAsync Task<OperateResult> UnSubscribeAsync(Address, CancellationToken) 停止订阅
GetBaseObjectAsync Task<OperateResult> GetBaseObjectAsync(CancellationToken) 返回底层通信对象
GetStatusAsync Task<OperateResult> GetStatusAsync(CancellationToken) 返回当前设备状态

2.2 WebApi 集成

DaqAbstract 通过 WAOn/WAOff/WAStatus 提供内置的 HTTP API 端点用于远程设备控制:

端点 HTTP 用途 参数
/api/on POST 打开设备连接
/api/off POST 关闭设备连接
/api/read POST 从地址读取 请求体中传入 Address 对象
/api/write POST 写入地址 请求体中传入 ConcurrentDictionary<string, WriteModel>
/api/getstatus POST 获取设备状态
/api/switchlanguage POST 切换中英文
// 为驱动程序激活 WebApi
var wam = new WAModel { IpAddress = "0.0.0.0", Port = 8080 };
await modbus.WAOnAsync(wam);

// 现在可通过 HTTP 访问:POST http://localhost:8080/api/read
// 请求体:{ "SN": "...", "AddressArray": [{"AddressName":"D100","AddressDataType":"Int"}] }

2.3 写入重载

三种写入重载用于不同的数据表示:

// 原始 (value, encoding) 元组
await driver.WriteAsync(new ConcurrentDictionary<string, (object, EncodingType?)>
{
    ["D100"] = (123, null),
    ["D101"] = (45.6f, null)
});

// 普通对象值(默认为 ASCII 编码)
await driver.WriteAsync(new ConcurrentDictionary<string, object>
{
    ["D100"] = 123,
    ["D101"] = 45.6f
});

// 带有显式 DataType 的 WriteModel(自动转换)
await driver.WriteAsync(new ConcurrentDictionary<string, WriteModel>
{
    ["D100"] = new WriteModel { Value = 99.1, AddressDataType = DataType.Float }
});

2.4 具体驱动程序

以下全部继承自 DaqAbstract<O, D>

PLC 驱动(32+ 种): Snet.Siemens(西门子)、Snet.Mitsubishi(三菱)、Snet.Omron(欧姆龙)、Snet.AllenBradley(AB)、Snet.Beckhoff(倍福)、Snet.CimonSnet.Delta(台达)、Snet.Fanuc(发那科)、Snet.Fatek(永宏)、Snet.FreedomSnet.Fuji(富士)、Snet.GE(通用电气)、Snet.Inovance(汇川)、Snet.Invt(英威腾)、Snet.Keyence(基恩士)、Snet.Kossi(顾美)、Snet.LSisSnet.MegMeet(麦格米特)、Snet.ModbusSnet.OrientalMotor(东方马达)、Snet.Panasonic(松下)、Snet.RKCSnet.Toyota(丰田)、Snet.Turck(图尔克)、Snet.Vigor(丰炜)、Snet.WeCon(维控)、Snet.XinJE(信捷)、Snet.Yamatake(山武)、Snet.Yaskawa(安川)、Snet.Yokogawa(横河)、Snet.YuDian(宇电)

其他 DAQ: Snet.DB(数据库驱动)、Snet.Sim(仿真设备)、Snet.TEP(TepMaster)、Snet.Opc(OpcDaClient、OpcDaHttp、OpcUaClient)


3. MqAbstract <O, D> -- 消息队列基类

文件: Snet.Core/abstract/MqAbstract.cs 继承: CoreUnify<O, D> 实现: IMq

消息队列/消息代理集成的基类。在异构 MQ 实现之上提供一致的生产/消费抽象。

3.1 抽象方法

方法 签名 用途
OnAsync Task<OperateResult> OnAsync(CancellationToken) 打开与代理的连接
OffAsync Task<OperateResult> OffAsync(bool, CancellationToken) 关闭连接
ProduceAsync Task<OperateResult> ProduceAsync(string topic, byte[], CancellationToken) 将二进制负载发布到主题
ConsumeAsync Task<OperateResult> ConsumeAsync(string topic, CancellationToken) 开始从主题消费
UnConsumeAsync Task<OperateResult> UnConsumeAsync(string topic, CancellationToken) 停止从主题消费
GetStatusAsync Task<OperateResult> GetStatusAsync(CancellationToken) 返回代理连接状态

3.2 字符串转字节的便捷方法

// 具体实现:使用默认 UTF-8 或指定编码将字符串序列化为 byte[]
public virtual Task<OperateResult> ProduceAsync(
    string topic, string content, Encoding? encoding = null, CancellationToken token = default)
{
    return ProduceAsync(topic,
        encoding == null ? Encoding.UTF8.GetBytes(content) : encoding.GetBytes(content));
}

3.3 具体 MQ 实现

协议
Snet.Mqtt MqttClientOperate MQTT 3.1.1 / 5.0
Snet.Kafka Kafka client Apache Kafka
Snet.RabbitMQ RabbitMQ client AMQP 0-9-1
Snet.NetMQ NetMQ client ZeroMQ(C# 原生)
Snet.Netty Netty client 基于 TCP 的自定义 MQ

3.4 使用示例

var mqttData = new MqttClientData
{
    IP = "broker.emqx.io",
    Port = 1883,
    ClientId = "snet-client-001"
};
var mqtt = await MqttClientOperate.InstanceAsync(mqttData);

await mqtt.OnAsync();
await mqtt.ProduceAsync("snet/data", "{\"temp\": 23.5}");
await mqtt.ConsumeAsync("snet/commands");
await mqtt.OffAsync();

4. CommunicationAbstract <O, D> -- 客户端通信基类

文件: Snet.Core/abstract/CommunicationAbstract.cs 继承: CoreUnify<O, D> 实现: ICommunication

客户端通信传输(出站连接)的基类。

4.1 抽象方法

方法 用途
OnAsync 连接到远程端点
OffAsync(bool hardClose) 断开连接
SendAsync(byte[]) 发送原始字节
SendWaitAsync(byte[], CancellationToken) 发送并等待响应
GetBaseObjectAsync 返回底层 socket/客户端对象
GetStatusAsync 返回连接状态

4.2 具体实现

文件 传输方式
SerialOperate communication/serial/SerialOperate.cs RS-232/RS-485
TcpClientOperate communication/net/tcp/client/TcpClientOperate.cs TCP/IP
WsClientOperate communication/net/ws/client/WsClientOperate.cs WebSocket
UdpClientOperate communication/net/udp/unicast/client/UdpClientOperate.cs UDP 单播
UdpMulticastOperate communication/net/udp/multicast/UdpMulticastOperate.cs UDP 组播
UdpBroadcastOperate communication/net/udp/broadcast/UdpBroadcastOperate.cs UDP 广播

4.3 资源清理

public override void Dispose()
{
    Off(true);      // 先硬关闭连接
    base.Dispose(); // 从单例池中移除,注销事件
}

5. CommunicationServiceAbstract <O, D> -- 服务端通信基类

文件: Snet.Core/abstract/CommunicationServiceAbstract.cs 继承: CoreUnify<O, D> 实现: ICommunicationService

服务端通信传输(入站监听)的基类。

5.1 抽象方法

方法 用途
OnAsync 启动服务端监听
OffAsync(bool hardClose) 停止服务端
SendAsync(byte[], string[]?) 向多个目标客户端发送字节
SendAsync(byte[], string) 向指定客户端发送字节
RemoveAsync(string[]) 移除/断开指定客户端连接
GetBaseObjectAsync 返回底层服务端对象
GetStatusAsync 返回服务端状态及已连接客户端

5.2 具体实现

文件
TcpServiceOperate communication/net/tcp/service/TcpServiceOperate.cs
WsServiceOperate communication/net/ws/service/WsServiceOperate.cs
UdpServiceOperate communication/net/udp/unicast/service/UdpServiceOperate.cs

6. 完整继承层次结构

CoreUnify<O, D>                              (Snet.Core/extend/CoreUnify.cs)
|
+-- DaqAbstract<O, D>                 (Snet.Core/abstract/DaqAbstract.cs) [IDaq]
|   +-- 所有 PLC 驱动(32+ 个包:Siemens、Mitsubishi、Omron、AB 等)
|   +-- Snet.DB(数据库)
|   +-- Snet.Sim(仿真)
|   +-- Snet.TEP(TepMaster)
|   +-- Snet.Opc(OpcDaClient、OpcDaHttp、OpcUaClient)
|
+-- CommunicationAbstract<O, D>       (Snet.Core/abstract/CommunicationAbstract.cs) [ICommunication]
|   +-- SerialOperate(串口操作)
|   +-- TcpClientOperate(TCP 客户端操作)
|   +-- WsClientOperate(WebSocket 客户端操作)
|   +-- UdpClientOperate(UDP 单播客户端操作)
|   +-- UdpMulticastOperate(UDP 组播操作)
|   +-- UdpBroadcastOperate(UDP 广播操作)
|
+-- CommunicationServiceAbstract<O,D> (Snet.Core/abstract/CommunicationServiceAbstract.cs) [ICommunicationService]
|   +-- TcpServiceOperate(TCP 服务端操作)
|   +-- WsServiceOperate(WebSocket 服务端操作)
|   +-- UdpServiceOperate(UDP 单播服务端操作)
|
+-- MqAbstract<O, D>                  (Snet.Core/abstract/MqAbstract.cs) [IMq]
|   +-- Snet.Mqtt  (MqttClientOperate)
|   +-- Snet.Kafka
|   +-- Snet.RabbitMQ
|   +-- Snet.NetMQ
|   +-- Snet.Netty (NettyClient)
|
+-- SubscribeOperate                  (Snet.Core/subscribe/SubscribeOperate.cs)
+-- SubscribeSource<T>                (Snet.Core/subscribe/core/SubscribeSource.cs)
+-- SubscribeService<T>               (Snet.Core/subscribe/core/SubscribeService.cs)
+-- HttpServiceOperate                (Snet.Core/communication/net/http/service/HttpServiceOperate.cs)
+-- HttpClientOperate                 (Snet.Core/communication/net/http/client/HttpClientOperate.cs)
+-- ChannelOperate<T>                 (Snet.Core/channel/ChannelOperate.cs)
+-- MqOperate                         (Snet.Core/mq/MqOperate.cs)
+-- ReflectionOperate                 (Snet.Core/reflection/ReflectionOperate.cs)
+-- VirtualAddress                    (Snet.Core/virtualAddress/VirtualAddress.cs)
+-- BytesHandler                      (Snet.Core/handler/BytesHandler.cs)
+-- ShareCacheOperate                 (Snet.Core/cache/share/ShareCacheOperate.cs)
+-- ProcessCacheOperate               (Snet.Core/cache/process/ProcessCacheOperate.cs)
+-- PluginOperate                     (Snet.Core/plugin/PluginOperate.cs)

7. 核心设计模式

7.1 配置即键的单例池

每个 CoreUnify 子类拥有一个 static ConcurrentDictionary<D, O>。配置对象 D 同时充当构造参数和字典键。深度比较确保等效配置返回相同实例。255 实例上限防止无限制的资源消耗。

7.2 OperateResult -- 统一返回信封

每个方法都返回 OperateResult,它包含:

  • Status(bool)-- 成功/失败指示器
  • Message(string)-- 人类可读的本地化描述
  • RunTime(int)-- 执行时间(毫秒)
  • ResultData(object?)-- 实际负载
// 成功
return OperateResult.CreateSuccessResult("读取完成", rawData);
// 失败
return OperateResult.CreateFailureResult("连接超时");

7.3 抽象方法 + 具体便捷实现

抽象基类定义了子类必须实现的异步契约。同步重载作为便捷包装,使用 GetAwaiter().GetResult()。此模式防止调用者在异步上下文中意外阻塞,同时仍为控制台应用和脚本提供简单的同步 API。

7.4 WebApi 远程控制

DaqAbstract 内嵌了一个 HTTP 服务器(HttpServiceOperate),将设备操作暴露为 REST 端点。这使得任何 DAQ 驱动程序都可以在无需额外基础设施的情况下进行远程控制。


8. 包依赖

包 / 命名空间 角色
Snet.Core 框架核心:所有抽象类、扩展类、通信、缓存、通道、MQ、插件、反射、订阅、虚拟地址
Snet.Model 数据模型、接口(IDaqIMqICommunication 等)、枚举、事件参数类型
Snet.Log 封装 Serilog 的日志抽象层
Snet.Utility 工具类(JSON、字节、反射、时间、比较)
Snet.{Vendor} 各厂商 PLC 驱动包(每个都继承 DaqAbstract
Snet.Mqtt MQTT 客户端(继承 MqAbstract
Snet.Kafka Kafka 客户端(继承 MqAbstract
Snet.RabbitMQ RabbitMQ 客户端(继承 MqAbstract
Snet.NetMQ NetMQ/ZeroMQ 客户端(继承 MqAbstract
Snet.Netty Netty 客户端(继承 MqAbstract
Snet.Opc OPC DA / OPC UA 客户端(继承 DaqAbstract
Snet.DB 数据库 DAQ 驱动(继承 DaqAbstract
Snet.Sim 仿真驱动(继承 DaqAbstract
Snet.TEP TepMaster 集成(继承 DaqAbstract

9. CoreUnify 实例生命周期

1. 实例创建
   InstanceAsync(param)
   ├── param 为 null?→ Activator.CreateInstance<D>()
   ├── semaphore.WaitAsync()
   ├── objList.TryGetValue(param)   → 精确匹配?返回现有实例
   ├── kvp.Key.Comparer(param)      → 深度匹配?返回现有实例
   ├── CreateInstance(param)        → ConstructorInfo.Invoke()
   ├── objList.TryAdd(param, inst)  → 添加到池中
   └── semaphore.Release()

2. 操作
   BegOperateAsync() → TimeHandler.StartRecord()
   ... 业务逻辑 ...
   EndOperateAsync(status, msg, data, ex)
   ├── TimeHandler.StopRecord()     → 计算 runTime
   ├── 格式化本地化日志消息
   ├── 如果 !status:通过 Serilog 调用 LogHelper.ErrorAsync()
   └── 返回 OperateResult(status, msg, runTime, data)

3. 释放
   DisposeAsync()
   ├── [子类清理:WAOff()、Off(true)]
   ├── UnregisterLanguageEvent()    → -= 缓存的委托
   ├── objList.Remove(basics)       → 从池中移除
   └── GC.SuppressFinalize()

10. 接口摘要

接口 定义位置 实现类 关键成员
IGetParam Snet.Model CoreUnify GetParamGetAutoAllocatingParam
ICreateInstance Snet.Model CoreUnify CreateInstance<T>CreateInstance(string json)
IEvent Snet.Model CoreUnify OnDataEventOnInfoEventOnLanguageEvent(+ async)
ILog Snet.Model CoreUnify LogOperateSetLogOperateGet
ILanguage Snet.Model CoreUnify GetLanguageValueSetLanguageGetLanguage
IDaq Snet.Model DaqAbstract 继承 IOnIOffIReadIWriteISubscribeIGetStatusIWAIGetObject
IMq Snet.Model MqAbstract 继承 IOnIOffIProducerIConsumerIGetStatus
ICommunication Snet.Model CommunicationAbstract OnOffSendSendWaitGetBaseObjectGetStatus
ICommunicationService Snet.Model CommunicationServiceAbstract OnOffSend(byte[],string[])Send(byte[],string)RemoveGetBaseObjectGetStatus

相关文档

  • Snet.Docs -- API 参考: F:/Snet/Web/Snet.Docs/wwwroot/docs/api/
  • Snet.Model: F:/Snet/Shunnet/Snet/Snet.Model/ -- 接口、枚举、数据模型
  • Snet.Log: F:/Snet/Shunnet/Snet/Snet.Log/ -- 基于 Serilog 的日志基础设施
  • Snet.Utility: F:/Snet/Shunnet/Snet/Snet.Utility/ -- 共享工具类
  • 单个驱动文档: 每个 Snet.{Vendor} 包均包含自己的 README