文件系统变化需通过filesystemwatcher捕获后包装为不可变、语义清晰的领域事件(如fileuploadedevent),经事件总线分发;须避免阻塞监听线程、添加去重与correlationid关联命令与事件,并采用双源校验保障可靠性。

文件系统变化怎么变成 CQRS 里的事件
Windows 上的 FileSystemWatcher 是最直接的入口,但它不是事件总线,也不能直接塞进 CQRS 的 IEvent 流程里。你得在它触发后,把原始通知包装成领域事件,再交给事件总线(比如 MediatR 或自建的 IEventPublisher)分发。
常见错误是直接在 Changed 回调里做业务逻辑或保存数据库——这会卡住文件监听线程,导致漏事件、重复触发,甚至 FileSystemWatcher 自动停止。
- 只在回调里提取关键信息:路径、变更类型(
Created/Changed/Deleted)、时间戳、是否是目录 - 用
Task.Run或await转到后台处理,避免阻塞FileSystemWatcher的内部线程池 - 对同一文件的连续修改(比如保存 Word 文档)会触发多次
Changed,加个简单去重缓存(如ConcurrentDictionary<string datetime></string>),500ms 内相同路径只发一次FileModifiedEvent
CQRS 事件类该怎么设计才不踩坑
别把 FileSystemEventArgs 直接当领域事件用。CQRS 要求事件是不可变、语义清晰、面向业务的——FileRenamedEvent 比 FileSystemChangedEvent 更好理解,也更利于后续重放或审计。
容易忽略的是版本和序列号:文件操作没有天然顺序,但 CQRS 事件流必须有序。建议在事件基类里加 SequenceNumber(由事件存储生成)和 OccurredAtUtc(用 DateTime.UtcNow,别用 Now)。
- 事件类必须是 public、无参构造、所有属性 get/set,否则序列化(如 JSON.NET 或 MessagePack)可能失败
- 路径字段统一用
string,不要存FileInfo或FileStream——它们不能跨进程/序列化 - 删除事件要包含原文件大小、哈希(如果之前上传时计算过),否则审计时无法确认删的是哪个版本
上传/修改/删除动作如何与文件事件对齐
用户点击“上传”不是文件事件的起点,而是命令(UploadFileCommand)。真正的事件源有两个:命令执行成功后显式发布 FileUploadedEvent,以及 FileSystemWatcher 捕获到磁盘写入完成后的 Created 事件。二者要能关联上——靠同一个 CorrelationId 字段。
典型场景:Web API 接收上传 → 保存到 uploads/ 目录 → 发布 FileUploadedEvent(含 CorrelationId)→ FileSystemWatcher 监听到该路径创建 → 发布带相同 CorrelationId 的 FileSyncedEvent。这样就能追踪“用户上传”到“磁盘落盘”的完整链路。
- 不要依赖
FileSystemWatcher的Created等于“上传完成”——大文件写入可能分块,Created只表示文件句柄打开,内容未必写完 - 修改场景同理:先发
FileUpdatedCommand,服务端改完再写磁盘,最后由监听器补发FileUpdatedEvent,而非监听Changed就发 - 删除操作必须走命令(
DeleteFileCommand),禁止前端直删磁盘;否则事件流断裂,审计日志缺失操作人、原因等上下文
FileSystemWatcher 在生产环境为什么经常失效
它不是为高可靠事件总线设计的:缓冲区默认只有 8KB,超量就丢事件;监视网络路径(SMB)基本不可靠;权限不足时静默失败,不抛异常;进程重启后监听丢失——这些都导致事件空洞。
真正能落地的方式是“双源校验”:以定期扫描(如每分钟查 Directory.GetFiles + FileInfo.LastWriteTimeUtc)作为兜底,和 FileSystemWatcher 输出做比对。差异项补发事件,并记录告警。
- 务必设置
NotifyFilter,只监听需要的类型(如NotifyFilters.FileName | NotifyFilters.LastWrite),减少内核通知压力 - 启用
IncludeSubdirectories = true时,子目录新增会触发两次事件(父目录Created+ 子目录Created),需在去重逻辑里一并处理 - Linux/macOS 不支持
FileSystemWatcher(.NET 6+ 有部分改进但仍有缺陷),跨平台项目必须用System.IO.Pipelines+inotify或第三方库如Libuv替代










