When working with Dynamics 365 Finance and Operations (D365 F&O), integrations often require us to send or receive files from external systems. One of the most common ways to do this securely is through SFTP (Secure File Transfer Protocol).
Unfortunately, D365 F&O doesn’t provide a native SFTP client out of the box. Instead, we have a few options to achieve this:
-
Use Azure Blob Storage + Logic Apps / Azure Functions for file movement (Microsoft recommended approach).
-
Use third-party libraries like Renci.SshNet with .NET interop inside X++.
In this post, we’ll focus on the direct SFTP approach using X++ and .NET interop.
Prerequisites: -
Before jumping into code, make sure you have the following in place:
-
Access to the SFTP server (host, port, username, password, and destination path).
-
The Renci.SshNet.Async library deployed to your D365 F&O environment (uploaded via Visual Studio project reference).
-
Proper permissions in your D365 F&O environment to run external .NET assemblies.
Step 3 - Create a class in C# Project and give it any name such as this case we have named it SFTPTransfer
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Renci.SshNet;
namespace SFTPFileTransfer
{
public class SFTPTransfer
{
public void TransferSFTPFile(string _host, string _username, string _password, System.IO.Stream _sourceFile,
string _destinationPath, int _port, string _fileName)
{
List <AuthenticationMethod> methods;
methods = new List<AuthenticationMethod>
{
new PasswordAuthenticationMethod(_username, _password)
};
try
{
var connectionInfo = new ConnectionInfo(_host, _port, _username, methods.ToArray());
using (SftpClient sftpclient = new SftpClient(connectionInfo))
{
sftpclient.Connect();
sftpclient.ChangeDirectory(_destinationPath.Trim());
_sourceFile.Position = 0;
sftpclient.BufferSize = 8 * 1024;
sftpclient.UploadFile(_sourceFile, _fileName);
}
}
catch (WebException ex)
{
}
}
}
}
using SFTPFileTransfer;
public class SFTPUploadTest
{
public void executeuploadtoSFTP(SrsReportRunController _controller)
{
str sftpServer = "abc.sftp.com";
int sftpPort = 22;
str sftpUser = "xyz";
str sftpPassword = "abcd12344";
str sftpPath = "/AB/Report/"; // remote directory
str fileName = 'VendorAgingReport.xlsx';
// Be careful with the fileName as few SFTP servers reject the file due to invalid characters in the name
// --- Generate Report as Excel file ---
SRSPrintDestinationSettings printSettings;
SRSReportRunService srsReportRunService;
Map reportParametersMap;
Microsoft.Dynamics.AX.Framework.Reporting.Shared.ReportingService.ParameterValue[] parameterValueArray;
SRSProxy srsProxy;
System.Byte[] reportBytes;
// Setup report
_controller.parmReportName(ssrsReportStr(VendAgingReport, DesignWithNoDetailAndNoTransactionCur)); // Vendor Aging Report
_controller.parmShowDialog(false);
_controller.parmExecutionMode(SysOperationExecutionMode::Synchronous);
// Print settings
printSettings = _controller.parmReportContract().parmPrintSettings();
printSettings.printMediumType(SRSPrintMediumType::File);
printSettings.fileFormat(SRSReportFileFormat::Excel);
// Run report service
srsReportRunService = new SRSReportRunService();
srsReportRunService.getReportDataContract(_controller.parmReportContract().parmReportName());
srsReportRunService.preRunReport(_controller.parmReportContract());
reportParametersMap = srsReportRunService.createParamMapFromContract(_controller.parmReportContract());
parameterValueArray = SrsReportRunUtil::getParameterValueArray(reportParametersMap);
_controller.parmReportContract().parmReportExecutionInfo(new SRSReportExecutionInfo());
_controller.parmReportContract().parmReportServerConfig(SRSConfiguration::getDefaultServerConfiguration());
srsProxy = SRSProxy::constructWithConfiguration(_controller.parmReportContract().parmReportServerConfig());
// Render report to byte array (Excel)
reportBytes = srsProxy.renderReportToByteArray(_controller.parmReportContract().parmReportPath(),
parameterValueArray,
printSettings.fileFormat(),
printSettings.deviceinfo());
if (!reportBytes || reportBytes.get_Length() == 0)
{
throw error("Vendor aging report rendering failed.");
}
System.IO.Stream objstream = new System.IO.MemoryStream(reportBytes);
// --- Upload to SFTP ---
SFTPTransfer transfer = new SFTPTransfer();
transfer.TransferSFTPFile(sftpServer,sftpUser,sftpPassword,objstream,sftpPath,sftpPort,fileName);
}
}
[ExtensionOf(classstr(VendAgingReportController))]
final class VendAgingReportController_Extension
{
protected void preRunModifyContract()
{
next preRunModifyContract();
SFTPUploadTest objsftpupload = new SFTPUploadTest();
objsftpupload.executeuploadtoSFTP(this);
}
}
That's all for now. Please let us know your questions or feedback in comments section !!!!
