Hey Team,
I’m trying to essentially implement Framework as a Library. This is in contrast to how UiPath implements Project frameworks, where once you initialize the project, the framework does not receive any updates. I want to create a coded framework that is extensible by the deriving or implementing class, as well as being version controlled through a Library. This would enable updating frameworks by importing updated versions of packages that implement the base classes for them as well!
Here is my current implementation that UiPath refuses to compile or test SOLELY due to the generics present in the Main subclass:
namespace BasicPerformer
{
public abstract class BaseConfig : DictionaryObject
{
public string ProcessName = "BasicPerformer";
public string QueueName = "BasicPerformer";
public string QueueFolder = "LazyFramework";
public string Folder_ExScreenshot = "Data\\Exception Screenshots";
public string[] ProcessesToKill = new string[0] { };
public TimeSpan Maintenance_Start = new TimeSpan(0, 0, 0);
public TimeSpan Maintenance_End = new TimeSpan(0, 0, 0);
public int MaxConsecutiveExceptions = 5;
public int MaxQueueRetries = 3;
public List<string> FrameEx_To = new List<string>();
public List<string> FrameEx_CC = new List<string>();
public List<string> TransSysEx_To = new List<string>();
public List<string> TransSysEx_CC = new List<string>();
public List<string> TransBusEx_To = new List<string>();
public List<string> TransBusEx_CC = new List<string>();
public string FrameEx_Subject = "";
public string FrameEx_Body = "";
public string TransSysEx_Subject = "";
public string TransSysEx_Body = "";
public string TransBusEx_Subject = "";
public string TransBusEx_Body = "";
}
public abstract class BaseStateData<TConfig> where TConfig : BaseConfig
{
public TConfig? Config = null;
public Exception? SysEx = null;
public Exception? FrameEx = null;
public BusinessRuleException? BusEx = null;
public QueueItem? Transaction = null;
public int ConsecutiveSystemExceptions = 0;
}
public abstract class BaseWorkflow<TState, TConfig> : CodedWorkflow where TState : BaseStateData<TConfig> where TConfig : BaseConfig
{
public abstract TState Execute(TState state);
}
public abstract class BaseMain<TConfig, TStateData, TWorkflow> : CodedWorkflow where TConfig : BaseConfig, new() where TStateData : BaseStateData<TConfig>, new() where TWorkflow : BaseWorkflow<TStateData, TConfig>
{
public delegate TStateData ExecuteDelegate(TStateData state);
public ExecuteDelegate? InitializeSettings;
public ExecuteDelegate InitializeApplications;
public ExecuteDelegate Process;
public ExecuteDelegate? HandleBusinessException;
public ExecuteDelegate? HandleSystemException;
public ExecuteDelegate? HandleSuccess;
public ExecuteDelegate? End;
public ExecuteDelegate CloseApplications;
public BaseMain(
)
{
InitializeApplications = (TStateData state) => {
Log("Base implementation");
return state;
};
Process = (TStateData state) => {
Log("Base implementation");
return state;
};
CloseApplications = (TStateData state) => {
Log("Base implementation");
return state;
};
}
public TStateData Data = new();
public Stack<State> Stack = new();
private void ValidateWorkflows()
{
if(InitializeApplications == null) throw new ArgumentNullException(nameof(InitializeApplications));
if(Process == null) throw new ArgumentNullException(nameof(Process));
if(CloseApplications == null) throw new ArgumentNullException(nameof(CloseApplications));
}
[Workflow]
public virtual void Execute(string configPath, string[] ignored)
{
ValidateWorkflows();
Log("The primary screen resolution is: " + SystemParameters.PrimaryScreenWidth.ToString() + " x " + SystemParameters.PrimaryScreenHeight.ToString());
if (InitializeSettings != null) Data = InitializeSettings(Data);
else Data = new TStateData() { Config = DictionaryObjectFactory.FromDictionary<TConfig>(workflows.LoadConfig(configPath, ignored)) };
Stack.Push(InitializeState);
while (Stack.Count > 0)
{
try
{
var currentState = Stack.Pop();
currentState.Invoke();
}
catch (Exception e)
{
Data.FrameEx = e;
Stack.Push(EndState);
}
}
}
public abstract void SendEmail(List<string>? to, List<string>? cc, List<string> attachments, string body, string subject);
public void SendErrorEmail()
{
if (Data.FrameEx != null)
{
var template = new DiagnosticDictionary(Data.FrameEx);
template["ProcessName"] = Data.Config?.ProcessName;
var attachments = new List<string>() { SharedHelpers.TakeScreenshot(Data.Config?.Folder_ExScreenshot) };
var (body, subject) = Shared.Email.Helpers.UpdateEmailTemplate(
Data.Config?.FrameEx_Body, Data.Config?.FrameEx_Subject, template._data
);
SendEmail(Data.Config?.FrameEx_To, Data.Config?.FrameEx_CC, attachments, body, subject);
}
else if (Data.SysEx != null)
{
var template = new DiagnosticDictionary(Data.SysEx);
template["ProcessName"] = Data.Config?.ProcessName;
var attachments = new List<string>() { SharedHelpers.TakeScreenshot(Data.Config?.Folder_ExScreenshot) };
var (body, subject) = Shared.Email.Helpers.UpdateEmailTemplate(
Data.Config?.TransSysEx_Body, Data.Config?.TransSysEx_Subject, template._data
);
SendEmail(Data.Config?.TransSysEx_To, Data.Config?.TransSysEx_CC, attachments, body, subject);
}
else if (Data.BusEx != null)
{
var template = new DiagnosticDictionary(Data.BusEx);
template["ProcessName"] = Data.Config?.ProcessName;
var attachments = new List<string>() { SharedHelpers.TakeScreenshot(Data.Config?.Folder_ExScreenshot) };
var (body, subject) = Shared.Email.Helpers.UpdateEmailTemplate(
Data.Config?.TransBusEx_Body, Data.Config?.TransBusEx_Subject, template._data
);
SendEmail(Data.Config?.TransBusEx_To, Data.Config?.TransBusEx_CC, attachments, body, subject);
}
else
{
throw new NotImplementedException("Unknown state for sending error email");
}
}
private bool IsMaintenanceTime()
{
return SharedHelpers.CurrentlyBetweenTimes(Data.Config?.Maintenance_Start, Data.Config?.Maintenance_End);
}
public delegate void State();
public void InitializeState()
{
Log("Entering Initialize...");
Data.SysEx = null;
try
{
Log("Trying to close applications");
Data = CloseApplications(Data);
}
catch
{
Log("Failed to close applications, killing processes instead");
SharedHelpers.KillProcesses(Data.Config?.ProcessesToKill ?? new string[]{});
}
if (IsMaintenanceTime())
{
Log("Within maintenance window");
Stack.Push(EndState);
}
{
SharedHelpers.Retry<bool>(() =>
{
Log("Initializing applications...");
Data = InitializeApplications(Data);
Log("Applications initialized");
});
Stack.Push(GetTransactionState);
}
}
public void GetTransactionState()
{
Log("Getting transaction");
if (IsMaintenanceTime())
{
Log("Currently within maintenance window");
Stack.Push(EndState);
return;
}
if (Data.Transaction == null)
{
Log("No more queue items");
Stack.Push(EndState);
return;
}
Log("Transaction found");
Stack.Push(ProcessState);
}
public void ProcessState()
{
ProcessingStatus outcome = ProcessingStatus.Successful;
Dictionary<string, object> analytics = new();
string details = "";
string reason = "";
ErrorType errorType = ErrorType.Application;
Data.BusEx = null;
try
{
Data = Process(Data);
}
catch (BusinessRuleException bre)
{
outcome = ProcessingStatus.Failed;
errorType = ErrorType.Business;
details = bre.Message;
reason = bre.StackTrace ?? "";
Data.BusEx = bre;
}
catch (Exception se)
{
outcome = ProcessingStatus.Failed;
errorType = ErrorType.Application;
reason = se.Message;
details = se.StackTrace ?? "";
Data.SysEx = se;
}
finally
{
system.SetTransactionStatus(
Data.Transaction,
outcome,
Data.Config?.QueueFolder,
analytics,
Data.Transaction?.Output,
details,
errorType,
reason,
15000
);
if (Data.SysEx != null)
{
SendErrorEmail();
Data = HandleSystemException != null ? HandleSystemException(Data) : Data;
Stack.Push(InitializeState);
}
else if (Data.BusEx != null)
{
SendErrorEmail();
Data = HandleBusinessException != null ? HandleBusinessException(Data) : Data;
Stack.Push(GetTransactionState);
}
else
{
Data = HandleSuccess != null ? HandleSuccess(Data) : Data;
Stack.Push(GetTransactionState);
}
}
}
public void EndState()
{
Data = End != null ? End(Data) : Data;
if (Data.FrameEx != null) SendErrorEmail();
try
{
Data = CloseApplications(Data);
}
catch
{
SharedHelpers.KillProcesses(Data.Config?.ProcessesToKill ?? new string[]{});
}
if (Data.FrameEx != null) throw Data.FrameEx;
}
}
This is my implementation:
public class Config : BaseConfig
{
public string NewProperty = "Test";
}
public class StateData : BaseStateData<Config>
{
public string NewField { get; set; } = "Test";
}
public abstract class Workflow : BaseWorkflow<StateData, Config>
{
}
public class Main : BaseMain<Config, StateData, Workflow>
{
[Workflow]
public override void Execute(
string? configPath,
string[]? ignored
)
{
configPath ??= Path.Combine(Directory.GetCurrentDirectory(), @"Performers\Basic\Data\BasicPerformerConfig.xlsx");
ignored ??= new string[] { };
base.Execute(configPath, ignored);
}
public override void SendEmail(List<string>? to, List<string>? cc, List<string> attachments, string body, string subject)
{
throw new NotImplementedException();
}
}