Crystal Reports并行生成PDF在IIS正常但NUnit测试抛COM异常
问题现象
在运行NUnit测试验证多份PDF报表生成功能时,抛出异常:System.Runtime.InteropServices.COMException : Invalid index. (Exception from HRESULT: 0x8002000B (DISP_E_BADINDEX))
但在Visual Studio本地IIS、专用Windows Server的IIS环境中运行代码时,报表生成完全正常。该系统是基于.Net Framework 4.8的Asp.Net MVC 5遗留系统。
相关代码示例
报表生成入口代码
public string GenerateReports(Query query) { // 收集报表数据 const string report_1_folderPath = "path1"; Task generateReports_Task_1 = Task.Run(() => { Parallel.ForEach(reportData_1, (data) => { _reportGenerator.GenerateReport_1(data, report_1_folderPath); }); }); const string report_2_folderPath = "path2"; Task generateReports_Task_2 = Task.Run(() => { Parallel.ForEach(reportData_2, (data) => { _reportGenerator.GenerateReport_2(data, report_2_folderPath); }); }); // 其他处理逻辑... Task.WaitAll(generateReports_Task_1, generateReports_Task_2); // 归档文件夹并返回给客户端... }
ReportGenerator类实现
public class ReportGenerator { public string GenerateReport_1(ReportData data, string folderPathToSaveReport) { using (ReportDocument report = new ReportDocument()) { report.Load(_reportTemplatePath); // data.DataSourceObject 类型为 IEnumerable<T> report.SetDataSource(data.DataSourceObject); report.SetParameterValue("Parameter1", data.Parameters.p1); // 所有线程执行此步骤均正常(data.Parameters.p1 为 string 类型) report.SetParameterValue("Parameter2", data.Parameters.p2); // 执行此步骤时抛出异常(data.Parameters.p2 为 string 类型) string outputFileFullPath = Path.Combine(folderPathToSaveReport, "someName.pdf"); report.ExportToDisk(CrystalDecisions.Shared.ExportFormatType.PortableDocFormat, outputFileFullPath); return outputFileFullPath; } } }
问题原因
已排除参数名称错误问题,经分析:
Crystal Reports 13依赖COM互操作机制,本身并非线程安全。使用Parallel.ForEach()并行生成报表时,尤其是在NUnit这类非Web上下文的测试环境中,极易引发COM异常、资源竞争、许可/上下文相关问题。IIS环境拥有配置完善的应用池和线程上下文,能够满足Crystal Reports的运行要求,而NUnit测试环境缺少这些必要的运行条件。
推荐解决方案
1. 序列化访问Crystal Reports引擎
通过锁机制确保同一时间只有一个线程访问Crystal Reports相关操作:
var reports = new ConcurrentBag<byte[]>(); Parallel.ForEach(dataList, data => { var reportBytes = GenerateReportThreadSafe(data); // 将CR调用包裹在锁内 reports.Add(reportBytes); });
private static readonly object crystalLock = new object(); private byte[] GenerateReportThreadSafe(MyData data) { lock (crystalLock) { // 创建并导出Crystal报表的逻辑 } }
2. 单元测试中避免并行逻辑
在测试用例中改用串行方式生成报表:
[Test] public void GenerateReports_Sequentially_WorksInTest() { var data = new[] { item1, item2, item3 }; foreach (var item in data) { var result = _reportService.Generate(item); // 不使用并行 Assert.That(result.Length, Is.GreaterThan(0)); } }
3. 使用Mock封装Crystal逻辑
抽象报表生成接口,测试时使用Mock对象替代真实的Crystal Reports逻辑:
public interface IReportGenerator { byte[] Generate(MyData data); }
var mockGenerator = new Mock<IReportGenerator>(); mockGenerator.Setup(x => x.Generate(It.IsAny<MyData>())) .Returns(new byte[] { 1, 2, 3 });
4. 规范测试环境中的临时文件使用
确保Crystal Reports不依赖未配置的临时文件或注册表项,显式设置报表导出路径,避免环境差异导致的问题。
5. 检查测试环境的CR运行时完整性
确认测试机器已安装完整的Crystal Reports运行时,且32位/64位设置与应用程序匹配。
总结
测试环境中不要并行运行Crystal Reports相关操作,通常应采用串行方式执行;高级场景可考虑创建独立的AppDomain或进程来隔离报表生成逻辑,避免资源竞争。
内容的提问来源于stack exchange,提问作者Nikola

