I have a function that checks to see if folders are available and since i upgraded from 2022.2 to 2023.2.5 i am getting this error so im not sure what i can do to fix it.
The ‘System.IO.Directory.Exists(string?)’ method cannot be called.
I have a function that checks to see if folders are available and since i upgraded from 2022.2 to 2023.2.5 i am getting this error so im not sure what i can do to fix it.
The ‘System.IO.Directory.Exists(string?)’ method cannot be called.
What’s the whole context of the function and do you get the error at compile time or execution time?
The context of the function is get all drawings that are attached to a part or PO and uses a BAQ see code below. This error i am getting when i try saving it.
var context = Ice.Services.ContextFactory.CreateContext<ErpContext>();
using (var svc = Ice.Assemblies.ServiceRenderer.GetService<Ice.Contracts.DynamicQuerySvcContract>(context))
{
//gets variables from Function
var PoNum = PONumber;
var RFQNum = RFQNumber;
var destFilePath = FilePath;
//BAQ ready for processing
Ice.Tablesets.DynamicQueryTableset dsQuery = svc.GetByID("API_GetPODrawgings");
Ice.Tablesets.QueryExecutionTableset dsBAQ = svc.GetQueryExecutionParameters(dsQuery);
if (PoNum != 0)
{
//Ice.Tablesets.DynamicQueryTableset dsQuery = svc.GetByID("API_GetPODrawgings");
//Ice.Tablesets.QueryExecutionTableset dsBAQ = svc.GetQueryExecutionParameters(dsQuery);
if (dsQuery != null)
{
dsBAQ.ExecutionParameter.Clear();
ExecutionParameterRow dsBAQRow = new ExecutionParameterRow();
dsBAQRow.ParameterID = "PONumber";
dsBAQRow.ParameterValue = PoNum.ToString();
dsBAQRow.IsEmpty = false;
dsBAQRow.SysRowID = Guid.NewGuid();
dsBAQRow.RowMod = "A";
dsBAQ.ExecutionParameter.Add(dsBAQRow);
ListOfFiles = svc.Execute(dsQuery,dsBAQ);
}
}
else if (RFQNum != 0)
{
Ice.Tablesets.DynamicQueryTableset dsQueryRFQ = svc.GetByID("API_GetRFQDrawings");
Ice.Tablesets.QueryExecutionTableset dsBAQRFQ = svc.GetQueryExecutionParameters(dsQuery);
if (dsQueryRFQ != null)
{
dsBAQRFQ.ExecutionParameter.Clear();
ExecutionParameterRow dsBAQRowRFQ = new ExecutionParameterRow();
dsBAQRowRFQ.ParameterID = "RFQNumber";
dsBAQRowRFQ.ParameterValue = RFQNum.ToString();
dsBAQRowRFQ.IsEmpty = false;
dsBAQRowRFQ.SysRowID = Guid.NewGuid();
dsBAQRowRFQ.RowMod = "A";
dsBAQRFQ.ExecutionParameter.Add(dsBAQRowRFQ);
ListOfFiles = svc.Execute(dsQueryRFQ,dsBAQRFQ);
}
}
}
List<string> FileList = new List<string>();
DataTable results = ListOfFiles.Tables["Results"];
foreach (DataRow row in results.Rows)
{
FileList.Add(row["XFileRef_XFileName"].ToString());
}
//creates Destination folder if does not exist
string destinationFolder = FilePath;
if (!Directory.Exists(destinationFolder))
{
Directory.CreateDirectory(destinationFolder);
}
//copy each file from the dataset to the destination folder
if (FileList !=null)
{
foreach (string file in FileList)
{
string sourceDirectory = Path.GetDirectoryName(file);
string uncSourceDirectory = sourceDirectory.Replace("V:\\","\\\\filesvr1\\connexuspdf\\");
//string sourceDirectory = @"\\filesvr1\connexusPDF\256\";
string fileName = Path.GetFileName(file);
string sourceFile = Path.Combine(uncSourceDirectory,fileName);
string destinationPath = Path.Combine(destinationFolder,fileName);
File.Copy(sourceFile,destinationPath,true);
}
}
else
{
DrawingsSucssfull = "No files to Copy";
}
Interesting so first its just a warning and I got it too but I wonder if Epicor is trying to sanitize some of the code and this is a madeup message by them.
A quick google doesn’t show this as a real compiler message and further more when I take the generated code and compile it myself it doesn’t give me that warnig.
@Olga or @pferrington or @Rich is this a new “feature”?
Confirmed that this is a compiler warning and not an error.
Here’s a simple script that generates a test file to the EpicorData user folder:
List<Ice.Lib.ServerPath.Types.PathInfo> pathInfoList;
string userFolder = String.Empty;
this.CallService<Ice.Contracts.ServerPathSvcContract>(hServerPath => {
pathInfoList = hServerPath.GetPaths(Epicor.ServiceModel.Utilities.SpecialFolder.UserData, "", false);
userFolder = pathInfoList[0].FullName;
});
string testPath = $"{userFolder}\\test.txt";
System.IO.File.WriteAllText(testPath,"test");
Function was promoted to production and run using Schedule Epicor Function. File was written successfully to the EpicorData user folder in spite of the compiler warnings.
Edit: I’ll bet the the desired implementation from Epicor is to use Ice.Contracts.Lib.FileTransfer, where any writing and reading is done through the service. UploadFile() sets the file data to a given byte and DownloadFile() loads the file to a byte in memory.
Do you see any replacement libraries that write to the EpicorData folder? Do you think this compiler warning is meant to get us to stop using System.IO and be prepared to use a “safer” object?
Exactly what I was simultaneously commenting. I think it is Ice.Contracts.Lib.FileTransfer.dll.
// Get Data from cloud storage
byte[] DownloadFile(SpecialFolder folder, string serverPath);
// Delete file
void FileDelete(SpecialFolder folder, string serverPath);
// File Exists
bool FileExists(SpecialFolder folder, string serverPath);
// Write Data to cloud storage
void UploadFile(SpecialFolder folder, string serverPath, byte[] data);
There are more access points but I think this gives the general idea.
Will that work for folks creating files in Functions or BPMs on the server-side?
Reading and writing data from the EpicorData user folder requires a reference to service ICE.Lib.FileTransfer
.
To read contents from a known file:
string fileName = "ThisIsARelativePath.txt";
string fileContent = String.Empty;
byte[] fileBytes;
this.CallService<Ice.Contracts.FileTransferSvcContract>(hFileTransfer => {
if(hFileTransfer.FileExists(Epicor.ServiceModel.Utilities.SpecialFolder.UserData, fileName))
{
fileBytes = hFileTransfer.DownloadFile(Epicor.ServiceModel.Utilities.SpecialFolder.UserData, fileName);
// If reading a simple data, csv, or text file
fileContent = Encoding.ASCII.GetString(fileBytes);
}
});
To create a new file with overwrite:
string fileName = "ThisIsARelativePath.txt";
string fileContent = "This is the full content of the file.";
byte[] fileBytes = Encoding.ASCII.GetBytes(fileContent);
this.CallService<Ice.Contracts.FileTransferSvcContract>(hFileTransfer => {
hFileTransfer.UploadFile(Epicor.ServiceModel.Utilities.SpecialFolder.UserData, fileName, fileBytes);
});
To delete a file if it exists:
string fileName = "ThisIsARelativePath.txt";
this.CallService<Ice.Contracts.FileTransferSvcContract>(hFileTransfer => {
hFileTransfer.FileDelete(Epicor.ServiceModel.Utilities.SpecialFolder.UserData, fileName);
});
To append to an existing file:
string fileName = "ThisIsARelativePath.txt";
string fileContent = "This is the content to append to an existing file.";
byte[] fileBytes, fileBytesAlreadyExisting;
byte[] fileBytesToAppend = Encoding.ASCII.GetBytes($"{Environment.NewLine}{fileContent}");
this.CallService<Ice.Contracts.FileTransferSvcContract>(hFileTransfer => {
if(hFileTransfer.FileExists(Epicor.ServiceModel.Utilities.SpecialFolder.UserData, fileName))
{
fileBytesAlreadyExisting = hFileTransfer.DownloadFile(Epicor.ServiceModel.Utilities.SpecialFolder.UserData, fileName);
fileBytes = new byte[fileBytesAlreadyExisting.Length + fileBytesToAppend.Length];
System.Buffer.BlockCopy(fileBytesAlreadyExisting, 0, fileBytes, 0, fileBytesAlreadyExisting.Length);
System.Buffer.BlockCopy(fileBytesToAppend, 0, fileBytes, fileBytesAlreadyExisting.Length, fileBytesToAppend.Length);
hFileTransfer.UploadFile(Epicor.ServiceModel.Utilities.SpecialFolder.UserData, fileName, fileBytes);
}
});
To create a new file or append if it exists with a line break:
string fileName = "ThisIsARelativePath.txt";
string fileContent = "This is the content to either add or append.";
byte[] fileBytes, fileBytesAlreadyExisting, fileBytesToAppend;
this.CallService<Ice.Contracts.FileTransferSvcContract>(hFileTransfer => {
if(hFileTransfer.FileExists(Epicor.ServiceModel.Utilities.SpecialFolder.UserData, fileName))
{
fileBytesToAppend = Encoding.ASCII.GetBytes($"{Environment.NewLine}{fileContent}");
fileBytesAlreadyExisting = hFileTransfer.DownloadFile(Epicor.ServiceModel.Utilities.SpecialFolder.UserData, fileName);
fileBytes = new byte[fileBytesAlreadyExisting.Length + fileBytesToAppend.Length];
System.Buffer.BlockCopy(fileBytesAlreadyExisting, 0, fileBytes, 0, fileBytesAlreadyExisting.Length);
System.Buffer.BlockCopy(fileBytesToAppend, 0, fileBytes, fileBytesAlreadyExisting.Length, fileBytesToAppend.Length);
hFileTransfer.UploadFile(Epicor.ServiceModel.Utilities.SpecialFolder.UserData, fileName, fileBytes);
}
else
{
fileBytes = Encoding.ASCII.GetBytes(fileContent);
hFileTransfer.UploadFile(Epicor.ServiceModel.Utilities.SpecialFolder.UserData, fileName, fileBytes);
}
});
We need some clarification from @Rich and or @pferrington but it seems to me they might be trying to keep us away from “unsafe” or threatening files / libraries that would let us do like system access type things particularly in SaaS
I’m not 100% sure how that’s going to work it’s hard to give us a programming language and the cripple some of its essential features. I think anyone with BPM code level access needs to understand the fact that they have a lot of power and can break the system Spider-Man rules apply and that’s good enough I really hope they aren’t going to knee cap Epicors greatest asset.
As long as it only applies to SaaS, it’s fine… Makes sense they wouldn’t want user code to start writing or reading files from the server in a SaaS context… If this also happen when on-premise however, we will have a problem…
Seriously, I think they’re more worried about poor or even malicious programmers than the good ones.
If Epicor is moving to containers, which are ephemeral, writing to server file locations may not be a great idea anyway. Also, I have been surprised on more than one upgrade where we moved the server (upgrade) and all kinds of broke because of hard coded file locations by previous coders planting these logic bombs in BPMs.
Honestly, all of you talented coders here will have no problem implementing things more securely. And less talented coders like myself (or malicious ones) will have some guardrails to protect our customers and companies.
List of Restrictions I was able to extract
Namespace | Type | Restricted State | Method/Constructor | Condition |
---|---|---|---|---|
System.IO | Directory | Restricted | - | - |
System.IO | DirectoryInfo | Restricted | - | - |
System.IO | DriveInfo | Restricted | - | - |
System.IO | File | Restricted | - | - |
System.IO | FileInfo | Restricted | - | - |
System.IO | FileStream | Restricted | - | - |
System.IO | FileSystemInfo | Restricted | - | - |
System.IO | FileSystemAclExtensions | Restricted | - | - |
System.IO | FileSystemWatcher | Restricted | - | - |
System.IO | Path | Restricted | GetFullPath | null |
GetTempFileName | null | |||
GetTempPath | null | |||
(Default Method Restriction) | RestrictedState.Unknown | |||
(Default Property Restriction) | RestrictedState.Unrestricted | |||
System.IO | RandomAccess | Restricted | - | - |
System.IO | StreamReader | Unknown | (Constructor) | FirstParameterIsString |
System.IO | UnmanagedMemoryAccessor | Restricted | - | - |
System.IO | UnmanagedMemoryStream | Restricted | - | - |
System.IO.Compression | ZipFile | Restricted | - | - |
System.IO.Compression | ZipFileExtensions | Restricted | - | - |
System.IO.Enumeration | - | Restricted | - | - |
System.IO.IsolatedStorage | - | Restricted | - | - |
System.IO.MemoryMappedFiles | - | Restricted | - | - |
System.IO.Pipelines | - | Restricted | - | - |
System.IO.Pipes | - | Restricted | - | - |
Guardrails are great, but if you are going to just slip some in…“PLEASE TELL US” instead of having to work it out for ourselves.
“Happy to be told that there was something put in the release notes and I just missed it”
I guess the next question is how far do you go in letting people know about the restrictions put in place in the interests of making things more secure…
Been trying to crack that nut in my own time. Anyone had any luck?
It still runs though. I thought the compiler warning was a good method of slowly changing things.
A good way to communicate, but.
“Method Cannot be called. Refer to Problem Number PRBXXXXXX”
At least this clearly indicates to the other person that this is not something that is broken, just changed, and take a look here for the detail.
Vague warnings, and messages drive me BONKERS!
Anyone for pressing the “Any Key”…I’ve been looking for that key on my keyboard for years!
Then there is no reason for restrictions, other than to annoy me.
These examples are very helpful. Thank you!
I’ve just encountered this ‘warning’ on a BPM that creates an XML file for a shipment. I inherited this BPM and am considering the most efficient way to re-code the BPM without System.IO. Currently, StreamWriter is used with MANY WriteLines. My first thought is to concatenate all the WriteLine values into a fileContent string and then call the FileTransferSvcContract. Is there another approach to this that I should consider?
Yes. I think you’re on the right track.
System.Text.StringBuilder is a very performance-efficient way to build a large string. Then, you can call a single write to create the file.
e.g.
System.Text.StringBuilder log = new System.Text.StringBuilder(String.Empty);
log.AppendLine("firstline");
log.AppendLine("secondline");
log.Append("thirdlinepart1");
log.Append("thirdlinepart2");
log.AppendLine("thirdlinepart3");
log.AppendLine("fourthline");
string finalText = log.ToString();