CQRS查询输入对象中IEnumerable的最佳构造实践探讨
在CQRS查询输入对象中正确处理IEnumerable参数的实践
在CQRS模式下,查询输入对象需要作为查询执行时的不可变快照,但直接使用IEnumerable<T>存在两个核心隐患:
- 若传入的是
List<T>这类可变集合的引用,外部可能在查询执行前修改集合内容,导致查询逻辑基于错误数据执行; - 若
IEnumerable<T>是延迟执行的LINQ表达式,查询执行时可能会重新计算,得到和输入对象创建时刻不同的结果。
先看你给出的原始最简实现:
public class GetVehiclesQueryInput { public GetVehiclesQueryInput(IEnumerable<int> vehicleIds = null) { VehicleIds = vehicleIds; } IEnumerable<int> VehicleIds { get; } }
这个实现的问题很明显:未对传入的vehicleIds做快照处理,完全依赖调用方行为,无法保证输入的稳定性。
以下是对你提出的两个优化方案的分析:
方案一:使用IReadOnlyCollection<T>结合只读包装器
public class GetVehiclesQueryInput { public GetVehiclesQueryInput(IEnumerable<int> vehicleIds = null) { VehicleIds = vehicleIds is null ? null : new ReadOnlyCollection<int>(vehicleIds.ToList()); } IReadOnlyCollection<int> VehicleIds { get; } }
核心优势:
- 立刻枚举传入的
IEnumerable<int>并转为List<int>,再用ReadOnlyCollection包装,彻底切断与原始集合的引用关联,同时禁止外部通过接口修改集合; - 无需引入额外NuGet包,
ReadOnlyCollection属于.NET基础类库; IReadOnlyCollection<T>暴露了Count属性,后续查询逻辑(比如判断是否为空来决定是否添加过滤条件)无需重复枚举,效率更高。
注意事项:
- 内部的
List<int>理论上可通过反射修改,但常规业务场景下这种情况极少; - 大型集合转换时会产生内存拷贝,但查询输入一般不会携带超大规模过滤参数,性能影响可忽略。
方案二:使用IImmutableList<T>(不可变集合)
public class GetVehiclesQueryInput { public GetVehiclesQueryInput(IEnumerable<int> vehicleIds = null) { VehicleIds = vehicleIds is null ? null : vehicleIds.ToImmutableList(); } IImmutableList<int> VehicleIds { get; } }
核心优势:
- 不可变集合的本质是一旦创建就无法修改,任何"修改"操作都会返回新集合实例,从根源上杜绝外部修改的可能;
IImmutableList<T>提供了比IReadOnlyCollection<T>更丰富的操作API,若后续需基于原始输入做衍生查询,无需再转换集合类型;- 语义更清晰:
IImmutableList直接传达"不可变快照"的意图,比IReadOnlyCollection的"仅接口只读"更具表现力。
注意事项:
- 需要引入
System.Collections.ImmutableNuGet包(.NET Core 2.0+/.NET 5+默认包含,.NET Framework需手动安装); - 不可变集合的创建和操作存在轻微性能开销,但查询输入场景下可忽略。
方案选择建议
- 若项目已在领域模型等场景使用不可变集合,优先选方案二,保持技术栈一致性;
- 若不想引入额外依赖或对包体积有要求,方案一是更务实的选择;
- 无论选哪种方案,核心原则都是:在查询输入对象的构造函数中,立刻将传入的
IEnumerable<T>转换为不可变/只读的快照集合,切断与原始集合的关联,禁止外部修改。
额外优化细节:可将null输入转换为空集合(比如ReadOnlyCollection<int>(new List<int>())或ImmutableList<int>.Empty),后续查询逻辑无需判断null,代码更简洁。
内容的提问来源于stack exchange,提问作者James B
相关产品推荐
相关产品推荐

