⚙️🖨️ 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:

  • 🖨️ Reports are executed directly against the Data Provider class.
  • 💾 Output is captured as a byte array → wrapped in a System.IO.MemoryStream.
  • 📂 No UI, no dialogs, no manual intervention.
  • 📦 Perfect for automation pipelines (e.g., packaging into ZIPs, emailing, archiving).

🧩 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:

  • You can read/write it just like a file.
  • You can pass it to other methods (e.g., ZIP creation, email attachments).
  • No need to save to disk unless you want to.

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:

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

  • Controller instance → Passed in with the modified contract (from Part 1). This ensures each iteration has the right contract context.
  • Print settings → Forced to PDF output with a file name, so the report knows how to render.
  • Report execution setup → Parameters are mapped from the contract into a parameter array.

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.

  • SRSProxy → Calls the SSRS engine to render the report design using the dataset provided by the RDP.
  • MemoryStream → Wraps the rendered PDF bytes so we can treat the report as an in‑memory file.

👉 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

  • Automation → Reports can run in batch jobs or background processes.
  • Flexibility → Streams can be zipped, emailed, or stored without touching disk.
  • Scalability → Works for any number of reports — each iteration produces a clean MemoryStream.
  • Security → No UI exposure, no manual handling of sensitive documents.

🔑 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 ;)

To view or add a comment, sign in

More articles by Anas Ahmad

Others also viewed

Explore content categories