Previous version:
I redid this dashboard to clean it up and add the ability to download multiple assemblies in one zip file.
For this dashboard to work, requires one layer, one function library, and one bpm.
The bpm is on Ice.Lib.FileTransfer.DownloadFile.
It is necessary because I do a little Tom Foolery on the file-transfer-kinetic widget. (use the serverpath to pass some data that we use to make this work)
Everything is in here → GetReferenceAssemblies.zip (18.8 KB)
Pics:
The relevant magic in the app..
Code:
BPM: Ice.Lib.FileTransfer.DownloadFile → Pre → GetReferenceAssemblies
/*
* ==========================================================================================
* AUTHOR: Kevin Lincecum
* COPYRIGHT: Kevin Lincecum 2026
* LICENSE: MIT
* ==========================================================================================
* Directive Type: Method -> Pre Processing
* Directive BO: Ice.Lib.FileTransfer.DownloadFile
* Directive Name: GetReferenceAssemblies
* Directive Desc: Intercepts the call to download a file if a custom url is used, and routes
* it appropriately.
* Directive Group: GetReferenceAssemblies
* ==========================================================================================
*
* Hi Mom!
*
* CHANGELOG:
* 04/02/2026 | klincecum | Kevin Lincecum | Initial Implementation
*
* ==========================================================================================
*/
//using Newtonsoft.Json;
//We are only interested in requests that the server path starts with "GRA://"
if(serverPath.StartsWith("GRA://"))
{
try
{
string json = serverPath.Replace("GRA://", String.Empty); //Pull off our trigger phrase
json = $"{{\"Assemblies\":{json}}}"; //Massage it into a DataSet
var ds = JsonConvert.DeserializeObject<DataSet>(json);
//Call the plugin function
var response = InvokeFunction("References", "DownloadAssemblies", ds);
//So y'all can see what was returned better
var responseUnwrapped = new
{
Success = (bool) response[0],
Message = (string)response[1],
ZipBase64 = (string)response[2]
};
//If we succeeded, we are done. Convert the Base64 Data from the function to a byte array and return it.
if( responseUnwrapped.Success ) //Success
{
if(!String.IsNullOrEmpty(responseUnwrapped.ZipBase64))
{
result = Convert.FromBase64String(responseUnwrapped.ZipBase64);
MarkCallCompleted();
}
return; //Done. Stop Processing.
}
//Failure
//Create an exception
var exception = new BLException( "Unable to download file." );
if(!String.IsNullOrEmpty(responseUnwrapped.Message))
{
//If we have errors, pass them along with the exception.
exception.Data.Add("Message", responseUnwrapped.Message);
}
//Bam!
throw exception;
}
catch (Exception ex)
{
//We will just rethrow anything. Catches our exceptions above, as well as unknowns.
throw new BLException(ex.ToString());
}
}
Functions:
Function: GetAvailableReferences
var FunctionID = this.ToString().Split(".").Last().Replace("Impl", "");
var Messages = new List<string>(); Messages.Add($"Begin {FunctionID} ->{Environment.NewLine}");
Action<string> AddMessage = (s) => {if(!(String.IsNullOrEmpty(s) || String.IsNullOrWhiteSpace(s))) {Messages.Add( $" {FunctionID}:{Environment.NewLine}" + string.Join(Environment.NewLine, s.Split(Environment.NewLine).Select(x => $" {x}")) + Environment.NewLine);}};
Func<object, string> Pretty = (o) => Newtonsoft.Json.JsonConvert.SerializeObject(o, Newtonsoft.Json.Formatting.Indented);
try
{
Assemblies = new DataSet();
var asmTable = Assemblies.Tables.Add("Assemblies");
asmTable.Columns.Add("AssemblyName", typeof(string));
asmTable.Columns.Add("FileName", typeof(string));
asmTable.Columns.Add("Version", typeof(string));
//Epicor.System missing, but available, wtf lol..
asmTable.Rows.Add("Epicor.System", "Epicor.System.dll", "");
CallService<Ice.Contracts.BpMethodSvcContract>(bpMethod =>
{
var bpMethodList = bpMethod.GetAvailableReferences("Assemblies");
//Message = Pretty(bpMethodList);
bpMethodList.ForEach(x =>
{
asmTable.Rows.Add(x.Name, x.FileName, x.Version);
});
});
Success = true;
}
catch (Exception ex)
{
AddMessage(ex.Message);
AddMessage(Pretty(ex));
}
finally
{
Messages.Add($"<- End {FunctionID}");
Message = String.Join(Environment.NewLine, Messages);
}
Function: DownloadAssemblies
var FunctionID = this.ToString().Split(".").Last().Replace("Impl", "");
var Messages = new List<string>(); Messages.Add($"Begin {FunctionID} ->{Environment.NewLine}");
Action<string> AddMessage = (s) => {if(!(String.IsNullOrEmpty(s) || String.IsNullOrWhiteSpace(s))) {Messages.Add( $" {FunctionID}:{Environment.NewLine}" + string.Join(Environment.NewLine, s.Split(Environment.NewLine).Select(x => $" {x}")) + Environment.NewLine);}};
Func<object, string> Pretty = (o) => Newtonsoft.Json.JsonConvert.SerializeObject(o, Newtonsoft.Json.Formatting.Indented);
try
{
Func<Dictionary<string, byte[]>, byte[]> ZipDictionary = (dicFiles) =>
{
byte[] retBytes = null;
using (MemoryStream zipMS = new MemoryStream())
{
using (ZipArchive zipArchive = new ZipArchive(zipMS, ZipArchiveMode.Create, true))
{
dicFiles.Keys.ToList().ForEach(df =>
{
var zipArchiveEntry = zipArchive.CreateEntry(df, CompressionLevel.Fastest);
using (var zipStream = zipArchiveEntry.Open())
{
zipStream.Write(dicFiles[df], 0, dicFiles[df].Length);
}
});
}
zipMS.Flush();
retBytes = zipMS.ToArray();
};
return retBytes;
};
var fileDictionary = new Dictionary<string, byte[]>();
CallService<Ice.Contracts.EcfToolsSvcContract>(ecf =>
{
Assemblies.Tables["Assemblies"].AsEnumerable().ToList().ForEach(x =>
{
var data = ecf.GetAssemblyBytes(x.Field<string>("AssemblyName"));
fileDictionary.Add(x.Field<string>("FileName"), data);
});
});
if(fileDictionary.Count == 0 ) return; //exit early
ZipBase64 = Convert.ToBase64String( ZipDictionary(fileDictionary) );
Success = true;
}
catch (Exception ex)
{
AddMessage(ex.Message);
AddMessage(Pretty(ex));
}
finally
{
Messages.Add($"<- End {FunctionID}");
Message = String.Join(Environment.NewLine, Messages);
}








