# 02. 核心领域模型与接口 本文档定义了 `DMS.Core` 项目的最终设计,它是整个系统的基石,包含所有业务实体和核心接口的定义。 ## 1. 目录结构 ``` DMS.Core/ ├── Enums/ │ ├── PollLevelType.cs │ ├── ProtocolType.cs │ └── SignalType.cs ├── Models/ │ ├── Device.cs │ ├── MqttServer.cs │ ├── Variable.cs │ ├── VariableMqttAlias.cs │ └── VariableTable.cs ├── Interfaces/ │ ├── IRepositoryManager.cs │ ├── IBaseRepository.cs │ ├── IDeviceRepository.cs │ ├── IMqttServerRepository.cs │ ├── IVariableMqttAliasRepository.cs │ ├── IVariableRepository.cs │ └── IVariableTableRepository.cs └── DMS.Core.csproj ``` ## 2. 核心接口 ### 2.1. `IRepositoryManager.cs` (工作单元模式) * **设计思路**:实现工作单元(Unit of Work, UoW)模式。它作为所有仓储的统一入口,并管理数据库事务。所有通过 `IRepositoryManager` 获取的仓储实例都共享同一个数据库上下文和事务。 * **优势**: * **原子性**:确保跨多个仓储的操作(如创建设备、变量表和菜单)要么全部成功,要么全部失败,维护数据一致性。 * **简化事务管理**:应用层无需直接与数据库事务API交互,只需调用 `BeginTransaction()`, `CommitAsync()`, `RollbackAsync()`。 * **解耦**:应用层不直接依赖具体的仓储实现,而是依赖于 `IRepositoryManager` 这一抽象。 * **资源优化**:在单个业务操作中,所有仓储共享同一个数据库连接,减少了连接开销。 * **劣势/权衡**: * **复杂性增加**:相比于直接使用仓储,引入UoW模式增加了额外的抽象层和概念。 * **仓储依赖**:`IRepositoryManager` 接口需要列出所有它管理的仓储,当新增仓储时,需要修改此接口。 ```csharp // 文件: DMS.Core/Interfaces/IRepositoryManager.cs namespace DMS.Core.Interfaces; /// /// 定义了一个仓储管理器,它使用工作单元模式来组合多个仓储操作,以确保事务的原子性。 /// 实现了IDisposable,以确保数据库连接等资源能被正确释放。 /// public interface IRepositoryManager : IDisposable { /// /// 获取设备仓储的实例。 /// 所有通过此管理器获取的仓储都共享同一个数据库上下文和事务。 /// IDeviceRepository Devices { get; } /// /// 获取变量表仓储的实例。 /// IVariableTableRepository VariableTables { get; } /// /// 获取变量仓储的实例。 /// IVariableRepository Variables { get; } /// /// 获取MQTT服务器仓储的实例。 /// IMqttServerRepository MqttServers { get; } /// /// 获取变量MQTT别名仓储的实例。 /// IVariableMqttAliasRepository VariableMqttAliases { get; } /// /// 获取菜单仓储的实例。 /// IMenuRepository Menus { get; } /// /// 开始一个新的数据库事务。 /// void BeginTransaction(); /// /// 异步提交当前事务中的所有变更。 /// /// 一个表示异步操作的任务。 Task CommitAsync(); /// /// 异步回滚当前事务中的所有变更。 /// /// 一个表示异步操作的任务。 Task RollbackAsync(); } ``` ### 2.2. 仓储接口 (`IBaseRepository.cs`, `IDeviceRepository.cs` 等) * **设计思路**:采用仓储(Repository)模式,为每个聚合根(或主要实体)定义一个数据访问接口。这些接口定义了对领域对象集合的操作,隐藏了底层数据存储的细节。 * **优势**: * **解耦**:将业务逻辑与数据访问技术(如SqlSugar、EF Core)分离,业务层不关心数据如何存储和检索。 * **可测试性**:可以轻松地为仓储接口创建Mock或Stub实现,便于单元测试业务逻辑。 * **领域驱动**:接口方法命名应反映领域语言,而不是数据库操作(如 `GetActiveDevicesWithDetailsAsync` 而非 `SelectFromDevicesJoinVariableTables`)。 * **劣势/权衡**: * **抽象开销**:对于简单的CRUD操作,引入仓储模式可能增加一些代码量和抽象层级。 * **过度抽象**:如果每个查询都定义一个方法,接口会变得非常庞大。需要权衡通用查询和特定业务查询。 ```csharp // 文件: DMS.Core/Interfaces/IBaseRepository.cs namespace DMS.Core.Interfaces; /// /// 提供泛型数据访问操作的基础仓储接口。 /// /// 领域模型的类型。 public interface IBaseRepository where T : class { /// /// 异步根据ID获取单个实体。 /// /// 实体的主键ID。 /// 找到的实体,如果不存在则返回null。 Task GetByIdAsync(int id); /// /// 异步获取所有实体。 /// /// 所有实体的列表。 Task> GetAllAsync(); /// /// 异步添加一个新实体。 /// /// 要添加的实体。 Task AddAsync(T entity); /// /// 异步更新一个已存在的实体。 /// /// 要更新的实体。 Task UpdateAsync(T entity); /// /// 异步根据ID删除一个实体。 /// /// 要删除的实体的主键ID。 Task DeleteAsync(int id); } // 文件: DMS.Core/Interfaces/IDeviceRepository.cs using DMS.Core.Models; namespace DMS.Core.Interfaces; /// /// 继承自IBaseRepository,提供设备相关的特定数据查询功能。 /// public interface IDeviceRepository : IBaseRepository { /// /// 异步获取所有激活的设备,并级联加载其下的变量表和变量。 /// 这是后台轮询服务需要的主要数据。 /// /// 包含完整层级结构的激活设备列表。 Task> GetActiveDevicesWithDetailsAsync(ProtocolType protocol); /// /// 异步根据设备ID获取设备及其所有详细信息(变量表、变量、MQTT别名等)。 /// /// 设备ID。 /// 包含详细信息的设备对象。 Task GetDeviceWithDetailsAsync(int deviceId); } ``` ## 3. 核心领域模型 * **设计思路**:领域模型是业务规则和数据的核心载体。它们是贫血模型(Anemic Domain Model),主要包含数据属性,行为逻辑则由应用服务和领域服务(如果需要)处理。模型之间通过导航属性(如 `List`)建立关系,反映业务实体间的关联。 * **优势**: * **清晰反映业务**:模型结构直接对应业务概念,易于理解和沟通。 * **可持久化**:模型可以直接或通过映射转换为数据库实体进行持久化。 * **技术中立**:不包含任何与特定技术(如数据库、UI)相关的代码。 * **劣势/权衡**: * **贫血模型争议**:一些DDD(领域驱动设计)倡导者认为贫血模型将数据和行为分离,导致领域逻辑分散。但在CRUD为主的应用中,这种简单性通常是可接受的。 * **对象图管理**:当模型之间存在复杂关系时,加载完整的对象图可能需要复杂的查询和映射。 ### `Device.cs` ```csharp // 文件: DMS.Core/Models/Device.cs /// /// 代表一个可管理的物理或逻辑设备。 /// public class Device { /// /// 唯一标识符。 /// public int Id { get; set; } /// /// 设备名称,用于UI显示和识别。 /// public string Name { get; set; } /// /// 设备使用的通信协议。 /// public ProtocolType Protocol { get; set; } /// /// 设备的IP地址。 /// public string IpAddress { get; set; } /// /// 设备的通信端口号。 /// public int Port { get; set; } /// /// S7 PLC的机架号。 /// public int Rack { get; set; } /// /// S7 PLC的槽号。 /// public int Slot { get; set; } /// /// 指示此设备是否处于激活状态。只有激活的设备才会被轮询。 /// public bool IsActive { get; set; } /// /// 此设备包含的变量表集合。 /// public List VariableTables { get; set; } = new(); } ``` ### `Variable.cs` ```csharp // 文件: DMS.Core/Models/Variable.cs /// /// 核心数据点,代表从设备读取的单个值。 /// public class Variable { /// /// 唯一标识符。 /// public int Id { get; set; } /// /// 变量名。 /// public string Name { get; set; } /// /// 在设备中的地址 (例如: DB1.DBD0, M100.0)。 /// public string Address { get; set; } /// /// 变量的数据类型。 /// public SignalType DataType { get; set; } /// /// 变量的轮询级别,决定了其读取频率。 /// public PollLevelType PollLevel { get; set; } /// /// 指示此变量是否处于激活状态。 /// public bool IsActive { get; set; } /// /// 所属变量表的ID。 /// public int VariableTableId { get; set; } /// /// 所属变量表的导航属性。 /// public VariableTable VariableTable { get; set; } /// /// 此变量的所有MQTT发布别名关联。一个变量可以关联多个MQTT服务器,每个关联可以有独立的别名。 /// public List MqttAliases { get; set; } = new(); /// /// 存储从设备读取到的最新值。此属性不应持久化到数据库,仅用于运行时。 /// [System.ComponentModel.DataAnnotations.Schema.NotMapped] // 标记此属性不映射到数据库 public object DataValue { get; set; } } ``` ### `VariableMqttAlias.cs` ```csharp // 文件: DMS.Core/Models/VariableMqttAlias.cs /// /// 领域模型:代表一个变量到一个MQTT服务器的特定关联,包含专属别名。 /// 这是一个关联实体,用于解决多对多关系中需要额外属性(别名)的问题。 /// public class VariableMqttAlias { /// /// 唯一标识符。 /// public int Id { get; set; } /// /// 关联的变量ID。 /// public int VariableId { get; set; } /// /// 关联的MQTT服务器ID。 /// public int MqttServerId { get; set; } /// /// 针对此特定[变量-服务器]连接的发布别名。此别名将用于构建MQTT Topic。 /// public string Alias { get; set; } /// /// 关联的变量导航属性。 /// public Variable Variable { get; set; } /// /// 关联的MQTT服务器导航属性。 /// public MqttServer MqttServer { get; set; } } ``` ## 4. 核心枚举 * **设计思路**:使用C#枚举来表示业务中固定的、有限的分类,如协议类型、信号类型、轮询级别。这提供了类型安全和代码可读性。 * **优势**: * **类型安全**:避免使用魔术字符串或整数,减少运行时错误。 * **代码可读性**:提高代码的自解释性。 * **易于维护**:所有可能的值集中管理,修改方便。 * **劣势/权衡**: * **扩展性**:如果枚举值需要频繁变动或由用户定义,则枚举可能不是最佳选择,可能需要考虑数据库驱动的查找表。 * **持久化**:枚举在数据库中通常存储为整数或字符串,需要注意映射和转换。 ### `PollLevelType.cs` ```csharp // 文件: DMS.Core/Enums/PollLevelType.cs /// /// 定义了变量的轮询级别,决定了其读取频率。 /// public enum PollLevelType { /// /// 不进行轮询。 /// Off, /// /// 高频轮询(例如:200ms)。 /// High, /// /// 中频轮询(例如:1000ms)。 /// Medium, /// /// 低频轮询(例如:5000ms)。 /// Low } ``` ### `ProtocolType.cs` ```csharp // 文件: DMS.Core/Enums/ProtocolType.cs /// /// 定义了设备支持的通信协议类型。 /// public enum ProtocolType { /// /// Siemens S7 通信协议。 /// S7, /// /// OPC UA (Unified Architecture) 协议。 /// OpcUa, /// /// Modbus TCP 协议。 /// ModbusTcp } ``` ### `SignalType.cs` ```csharp // 文件: DMS.Core/Enums/SignalType.cs /// /// 定义了变量支持的数据类型。 /// public enum SignalType { /// /// 布尔值 (true/false)。 /// Boolean, /// /// 字节 (8-bit 无符号整数)。 /// Byte, /// /// 16位有符号整数。 /// Int16, /// /// 32位有符号整数。 /// Int32, /// /// 单精度浮点数。 /// Float, /// /// 字符串。 /// String } ```