⚙️🖨️ Rendering Reports to MemoryStream in D365FO (No startOperation Needed!)
🚀 This is Part 2 of my trio series on automating SSRS reports in Dynamics 365 F&O. In Part 1, I explained how to dynamically modify the contract class so that each customer tied to a salesman generates a separate dataset and report. Now, let’s go deeper: how to render those reports directly into a MemoryStream without ever calling the startOperation function of SrsReportRunController.
🔎 Why This Matters
Normally, SSRS reports in D365FO are executed via the controller’s startOperation method, which triggers the UI and displays the report. But in automation scenarios, we don’t want dialogs or manual clicks — we want reports to run silently in the background and return as raw PDF bytes we can process programmatically.
That’s where MemoryStream rendering comes in:
🧩 What is a MemoryStream?
A MemoryStream is a .NET object that stores binary data (like a PDF file) directly in memory. Think of it as a virtual file:
In our case, each SSRS report is rendered into a MemoryStream → giving us a lightweight, in‑memory PDF that can be packaged, sent, or processed further.
💻 X++ Code
Here’s the key method:
Recommended by LinkedIn
public static System.IO.MemoryStream getFileStreams(DemoReportController controller, Filename fileName)
{
SRSProxy srsProxy;
Map reportParametersMap;
SRSPrintDestinationSettings settings;
System.Byte[] reportBytes = new System.Byte[0]();
SRSReportRunService srsReportRunService = new SrsReportRunService();
SRSReportExecutionInfo executionInfo = new SRSReportExecutionInfo();
Microsoft.Dynamics.AX.Framework.Reporting.Shared.ReportingService.ParameterValue[] parameterValueArray;
settings = controller.parmReportContract().parmPrintSettings();
settings.printMediumType(SRSPrintMediumType::File);
settings.fileName(fileName);
settings.fileFormat(SRSReportFileFormat::PDF);
controller.parmReportContract().parmReportServerConfig(SRSConfiguration::getDefaultServerConfiguration());
controller.parmReportContract().parmReportExecutionInfo(executionInfo);
srsReportRunService.getReportDataContract(controller.parmReportContract().parmReportName());
srsReportRunService.preRunReport(controller.parmReportContract());
reportParametersMap = srsReportRunService.createParamMapFromContract(controller.parmReportContract());
parameterValueArray = SrsReportRunUtil::getParameterValueArray(reportParametersMap);
srsProxy = SRSProxy::constructWithConfiguration(controller.parmReportContract().parmReportServerConfig());
reportBytes = srsProxy.renderReportToByteArray(
controller.parmReportContract().parmReportPath(),
parameterValueArray,
settings.fileFormat(),
settings.deviceInfo()
);
return new System.IO.MemoryStream(reportBytes);
}
🔄 What’s Happening Here
Data Provider (RDP) class → The contract values (like Customer, Salesman) are injected into the RDP class. The RDP executes its processReport() method, fetching data from tables (e.g., CustTable) based on the contract.It prepares the dataset that the SSRS report design consumes. In this flow, the RDP is still fully responsible for data retrieval and shaping, but instead of sending the dataset to the UI, it’s passed directly to the rendering pipeline.
👉 The key difference here: we bypass startOperation function. Normally, startOperation would trigger and run the RDP → dataset → SSRS → viewer pipeline. In this approach, the RDP still executes, but the output is captured silently as a MemoryStream, ready for automation (ZIP packaging, emailing, archiving, etc.).
🚀 Why This Is Powerful
🔑 Closing Thought
By rendering reports directly to MemoryStream, we unlock the ability to fully automate SSRS reporting in D365FO. Combined with Part 1 (dynamic contract modification), this forms the backbone of multi‑scenario reporting automation.
📌 Reference to Part 3, where I’ll dive deeper into ZIP packaging — showing how to take these MemoryStreams and bundle them into a single distributable file.
Thanks ;)
How