Creating Algorithms
This article highlights how to create algorithms in the platform that when initiated by a user can query the entire system, run custom processes, and update data all within a single transaction. It covers the following major points:
- Creating simple algorithms that act within a context from within the UI
- Creating complex algorithms that query data, and update it
- Enhancing an algorithm so that it can be run as a remote process on a task server as a Server Task
Tip
This article assumes you are familiar with the following LemonEdge concepts:
- Creating An AddIn: How to create a .net dll referencing the LemonEdge dlls.
- Designing Entities: How to create entities in the platform that we can then create algorithms against
- Designing UIs: How to create views and more to interact with our entities, and to which we can add custom commands to run our algorithms
Note
You can follow along with this article by downloading the API Examples projects which provide the code examples discussed here.
Algorithm Concepts
Any algorithm you create in LemonEdge will always have access to the following classes:
- A Context: An Entity Updater or Entity Retriever context. These can be used to query the LemonEdge platform and all its data including running custom queries created using our query tools. If you have the updater context you can use that to log changes with the data and commit those changes in one transactional batch.
- A User: A User Info class holding the details of the current user this algorithm is running under.
- A Local Cache: A Cache that can be used to access already loaded standing data to load configuration without having to make round trips to the database/service.
Tip
When writing your algorithm you can reference these interfaces, and your entities to accomplish any task with your data you like.
Best of all, you can take these algorithms and re-use the code anywhere else by replacing, or implementing, those simple interfaces to ensure you have maximum portability. Likewise you can take any existing proprietary algorithms you already have and easily integrate them into our API by refactoring your code to retrieve and update data with our context interface reference.
Our goal with having your algorithms on LemonEdge cover the following main aims:
- Ensuring you maintain complete ownership and control over your proprietary processes and algorithms
- Maximising your ability to port existing algorithms into our API
- Maximising the portability of your algorithms defined against our API to ensure you can re-use them elsewhere
- Ensuring your algorithms can fully integrate into our product and be accessible from user action, automatic processes, server tasks or however you would like them to run
- Providing all the power of our platform to your algorithms without you having to write an additional line of code. For instance all the entities you query, or update, with your algorithm instantly get these benefits for free:
- Integrates with Canvases
- Integrates with our Reporting tools
- Integrates with our security and permissions
- Ensures all changes are fully audited and accessible
- And much more...
Executing An Algorithm From A Command
The simplest algorithm to operate is one that runs within the context of the entities/data tje user is currently viewing. In other words a simple command that executes against the current item.
As an example we will be using the IPolicy entity created in the Designing Entities walkthrough, to create a simple command that increments the price of a policy by 100. We can do that using the following simple view command implementation:
public class SimplePolicyPriceIncrement : LemonEdge.Client.Core.Commands.ViewCommands.ViewCommand
{
private Func<Policy.Core.Interfaces.IPolicy> _getPolicy;
public SimplePolicyPriceIncrement(PolicyController controller) : base(controller.View.Displayer) => _getPolicy = () => controller.SingleItem;
public override bool InternalCanExecute(object parameter) => _getPolicy() != null;
public override void InternalExecute(object parameter)
{
var policy = _getPolicy();
var incrementor = new PolicyIncrementor(policy);
incrementor.IncrementPrice();
}
public override Guid IconID => LemonEdge.Client.Core.Images.ImageType.Plus.ToShortGuid();
public override string Title => "Increment";
public override string Description => "Adds 100 to Policy Price";
}
public class PolicyIncrementor
{
private Policy.Core.Interfaces.IPolicy _policy;
public PolicyIncrementor(IPolicy policy) => _policy = policy;
public decimal IncrementPrice() => _policy.Price += 100;
}
Tip
Although this is a very simple algorithm it is always worth abstracting your algorithm out so you have the maximum ability to re-use it elsewhere and keep your implementations portable. To hook your algorithms into the LemonEdge API you'll only ever really need to use the context interface to retrieve/update entities. The entities themselves, and your own algorithms, are all code you should be able to re-use anywhere just by swapping the context interface implementation to use any other 3rd party system instead. So in this example our PolicyIncrementor is 100% reusable anywhere else as the IPolicy interface and Incrementor class are all code we can use with other 3rd party systems that also have the same type of policies.
Our Commands guide goes into more detail about how to implement and use a command inheriting ViewCommand. But in essence all you really need to provide is a default icon, title and description so the application knows how to create your command, along with an implementation of the InternalCanExecute and InternalExecute methods (or their equivalent async versions).
The PolicyIncrementor algorithm the SimplePolicyPriceIncrement command uses is very simple and self explanatory. Though we will be adding to this later.
Important
An important point is we are not saving the change with this algorithm, the user can discard or override the change from the application. LemonEdge automatically tracks changes made against entities that are displayed against the standard view controllers. It will automatically detect the change in price, and enable the "Save" menu command along with visually indicating there is a change against the policy in the UI. The user can then override the change, run the command again, refresh and discard the changes, or hit save and accept them (along with any other manual changes). This is why our command itself is so simple and just lets us run the algorithm, we don't need to notify the system that items have changed, or save them. We will be looking at all these options in the next steps though.
In order to make the algorithm usable by the user, we are going to add it as a command to the policy view itself. We can do that by overriding the InitCommands method to add in our new command. If we take the Policy controller (created in the Designing UIs guide previously), we can simply enhance that like so:
public class PolicyController : BaseDefaultSingleViewController<IPolicy>
{
public PolicyController(IBaseDefaultSingleView<IPolicy> view) : base(view) { }
protected override IEnumerable<ControlDisplayInfoLight> ControlNames() =>
new ControlDisplayInfoLight[]
{
new ControlDisplayInfoLight(nameof(IPolicy.Name), editable: true) { Width = ControlDisplayInfo.DEFAULT_CONTROL_WIDTH_NORMAL},
new ControlDisplayInfoLight(nameof(IPolicy.CurrencyID), editable: true) { Width = ControlDisplayInfo.DEFAULT_CONTROL_WIDTH_NORMAL},
new ControlDisplayInfoLight(nameof(IPolicy.CountryID), editable: true) { Width = ControlDisplayInfo.DEFAULT_CONTROL_WIDTH_NORMAL},
new ControlDisplayInfoLight(nameof(IPolicy.Price), editable: true) { Width = ControlDisplayInfo.DEFAULT_CONTROL_WIDTH_NORMAL, Format = "n2"},
new ControlDisplayInfoLight(nameof(IPolicy.Description), editable: true) { Width = ControlDisplayInfo.DEFAULT_CONTROL_WIDTH_LARGE},
};
protected override void InitCommands(IList<ViewCommand> commands)
{
base.InitCommands(commands);
commands.Add(new SimplePolicyPriceIncrement(this));
}
}
Now if you open a policy in any of the LemonEdge clients (Windows Desktop Application, Admin Console, Web Browser, or Universal Core App) you can click the "Increment" button on the policy view and it will increment the price by 100.
Now we've added a simple UI algorithm we want to look at making it a bit more complex and try querying data in the same process.
Querying Data In Algorithms
We will need to use the Entity Updater or Entity Retriever context in order to query the LemonEdge system, and pull data into our algorithm. Wherever our algorithm runs we will always have access to a context in order to do this, or can create a new one using Connector.Create.
Tip
We can query data from LemonEdge using any of the following methods through the API:
- QueryableExecuter: Use the context to directly query the entities using our QueryableExecuter ensuring your queries work over the OData service or directly connected to the database.
- CustomServiceQueryExtender: Use the CustomServiceQueryExtender to create a LINQ query that can be called from the context and the results returned.
- SQL Wrappers: Use our query tools to create a SQL Wrapper that holds the actual sql of the query you want to execute.
- You can create this sql wrapper using custom sql, or our Dataset or other reporting tools, to build a query for your data that will generate a SQL Wrapper for you.
- You can execute any sql wrapper from the context.
- The result can be returned as a collection of SQL Wrapper Results
- You can interrogate the data using the SQL Wrapper Interpretor
- The result can be returned as a collection of custom Complex Types that have properties mapping to the results of your sql wrapper
- The result can be returned as a collection of SQL Wrapper Results
See the header links for more detail on using those methods to query your data. The key point is you have several ways to create quick and easy queries of your data, from LINQ all the way to custom sql all of which work across the web service or directly connected to the database.
For the purpose of this example we're going to create a very simple query which retrieves the largest Price across all policies:
public class PolicyIncrementor
{
private Policy.Core.Interfaces.IPolicy _policy;
private IEntityRetriever _context;
public PolicyIncrementor(IEntityRetriever context, IPolicy policy) => (_context, _policy) = (context, policy);
public decimal IncrementPrice() => _policy.Price += 100;
public async Task<decimal> IncrementAboveHighestPrice()
{
var highest = await GetHighestPrice();
if (highest > _policy.Price) _policy.Price = highest;
return IncrementPrice();
}
public async Task<decimal> GetHighestPrice()
{
var query = _context.GetItems<IPolicy>().OrderBy(nameof(IPolicy.Price), LemonEdge.Utils.Database.Order.Descending).Top(1);
var top = (await _context.ExecuteQuery(query)).FirstOrDefault();
return top == null ? 0 : top.Price;
}
}
You can see here we are simply creating a query that orders policies by their price in descending order, and then just taking the top result. This is all contained within the GetHighestPrice method. If you ever needed to make this algorithm work elsewhere you can re-implement that method, or create a class that implements IEntityRetriever to query the data from any 3rd party system.
This GetHighestPrice could also be implemented using the CustomServiceQueryExtender, or SQL Wrapper, methods to query the context. Given the simplicity of the query we don't need to create any custom sql to do this though.
Important
The queries are also running taking account all of the following for you:
- The Account they are running in (if running in a multi-tenanted setup)
- Any As Of Date applied to the context
- The Canvas (if any) that the context is operating within
- The Permission set for the Team the Current User is actively using
Next we just need to tweak the command itself so we use the Async override for execute:
public class SimplePolicyPriceIncrement : LemonEdge.Client.Core.Commands.ViewCommands.ViewCommand
{
private Func<Policy.Core.Interfaces.IPolicy> _getPolicy;
private Func<IEntityRetriever> _getContext;
public SimplePolicyPriceIncrement(PolicyController controller) : base(controller.View.Displayer) =>
(_getPolicy, _getContext) = (() => controller.SingleItem, () => controller.View.Displayer.Controller.MainDisplayedItem.Context );
public override bool InternalCanExecute(object parameter) => _getPolicy() != null;
public override async Task InternalExecuteAsync(object parameter)
{
var policy = _getPolicy();
var context = _getContext();
var incrementor = new PolicyIncrementor(context, policy);
await incrementor.IncrementAboveHighestPrice();
}
public override Guid IconID => LemonEdge.Client.Core.Images.ImageType.Plus.ToShortGuid();
public override string Title => "Increment";
public override string Description => "If this price is smaller than the highest price across all policies then it sets it to the highest + 100, else it just adds 100.";
}
If we assume you already have a policy with a price of 12345, if you create a new policy in any of the LemonEdge clients (Windows Desktop Application, Admin Console, Web Browser, or Universal Core App) you can click the "Increment" button on the policy view and it will change the price to 12345 + 100 = 12445. (Assuming you have no other policies with a price greater than 12345).
This illustrates how easy it is to incorporate querying any LemonEdge data into your algorithm. The important part is to note how you have full access to querying all entities and their properties throughout the entire system.
Now we'll look to enhance the algorithm to automatically save our changes too.
Updating Data In Algorithms
We will need to use the Entity Updater context in order to update the LemonEdge system. Wherever our algorithm runs we will always have access to a context in order to do this, or can create a new one using Connector.Create.
Warning
If we use the same context as above, then we'll be operating within the context of the view the user is looking at the policy from. This means if they changes the system, then update the price using our algorithm and our algorithm saved it, then it would save all changes in the context including the description change. Typically we wouldn't want that behaviour, so instead we'll create a new algorithm that will run standalone which will increase the policy with the highest price by 100. This way we'll create a new context for our work and it will be isolated from any other changes the user is currently making.
We'll make this command run from the grid of policies, so you can select it to increment the price of the largest policy. We'll also make it behave as it did previously if you select an actual policy so you can see the algorithm working from within the view context, and also out side it.
Caution
The important point about the algorithm running in the view context when you select a policy is that you'll notice the queries include the altered data within that context. In other words if you increment a policy (say A) to 10000 and that is the highest price now, if you don't save it and select another policy and increment it, it will become 10100 as the query will return results combined with your local results. If you want the results of any query not to be mixed with the current context then you need to create a new context to run the query and dispose of it after.
We can create this simple algorithm by creating a new incrementor like so:
public class HighestPolicyIncrementor
{
private IEntityUpdater _context;
public HighestPolicyIncrementor(IEntityUpdater context) => _context = context;
public async Task IncrementHighestPrice()
{
var query = _context.GetItems<IPolicy>().OrderBy(nameof(IPolicy.Price), LemonEdge.Utils.Database.Order.Descending).Top(1);
var top = (await _context.ExecuteQuery(query)).FirstOrDefault();
if (top != null)
{
top.Price += 100;
_context.LogChange(top, EntityOperation.Update); //Log our change
}
}
private async Task<decimal> GetHighestPrice()
{
var query = _context.GetItems<IPolicy>().OrderBy(nameof(IPolicy.Price), LemonEdge.Utils.Database.Order.Descending).Top(1);
var top = (await _context.ExecuteQuery(query)).FirstOrDefault();
return top == null ? 0 : top.Price;
}
}
This one simply needs a context as it does all its work using that single context only.
Tip
You'll notice the algorithm marks the changes to the entity using the LogChange method. You can use this to associate any changes you've made to the current context. When you've made all your changes you can call SaveChanges and the system will commit all your changes in a single transaction (Assuming validation, permissions and other processes pass).
Next we want a simple command we can add to the policies grid in order to use this new algorithm:
public class SimpleHighestPolicyPriceIncrement : LemonEdge.Client.Core.Commands.ViewCommands.ViewCommand
{
private Func<Policy.Core.Interfaces.IPolicy> _getPolicy;
private Func<IEntityUpdater> _getContext;
private Action<System.ComponentModel.INotifyPropertyChanged> _trackItem;
public SimpleHighestPolicyPriceIncrement(PoliciesGridController controller) : base(controller.View.Displayer) =>
(_getPolicy, _getContext, _trackItem) =
(() => controller.SelectedItemForSubViews as IPolicy,
() => controller.View.Displayer.Controller.MainDisplayedItem.Context,
controller.View.Displayer.Controller.TrackItem);
public override bool InternalCanExecute(object parameter) => true;
public override async Task InternalExecuteAsync(object parameter)
{
var policy = _getPolicy();
var context = _getContext();
if(policy == null)
{
//increment highest and save
using(var cn = await LemonEdge.Client.Core.CommonUI.Connector.Create())
{
var incrementor = new HighestPolicyIncrementor(cn);
await incrementor.IncrementHighestPrice();
await cn.SaveChanges();
}
}
else
{
//Unlike a view, the grid does not automatically track changes of an entity unless it has been selected and edited by a user.
//so we tell the displayer to track the changes of this item as we're going to alter it
_trackItem(policy);
//increment selected
var incrementor = new PolicyIncrementor(context, policy);
await incrementor.IncrementAboveHighestPrice();
//Had we not tracked the changes then we would have to notify the context manually that the item had changed, like so:
//context.LogChange(policy, EntityOperation.Update);
//And also inform the UI the item has changed so it would know to mark the tab (with a colour or *, or other) that the context has changes in it and thus enables the save command
//We'd need the controller reference to do that, which we didn't store locally but would need to in order to do this
//controller.View.Displayer.Controller.FireTrackedItemListeners();
//However seeing as we did track the changes the displayer takes care of all this for us automatically.
}
}
public override Guid IconID => LemonEdge.Client.Core.Images.ImageType.Plus.ToShortGuid();
public override string Title => "Increment";
public override string Description => "If a policy is selected and its price is smaller than the highest price across all policies then it sets it to the highest + 100, else it just adds 100. If no policy is selected the highest policy price is incremented by 100.";
}
You'll notice this is pretty much exactly the same as the SimplePolicyPriceIncrement command we created earlier with the addition that we track the item when they've selected it from a grid (as unlike the individual policy view the system doesn't automatically track all grid items), and that we also run the new algorithm if no policy is selected in the grid.
Otherwise, it's really as simple as that.
Lastly we add the command to our policies grid:
public class PoliciesGridController : BaseGridController<IPolicy>
{
public PoliciesGridController(IBaseGrid<IPolicy> view) : base(view) { }
protected override IEnumerable<ControlDisplayInfoLight> ColumnNames() =>
new ControlDisplayInfoLight[]
{
new ControlDisplayInfoLight(nameof(IPolicy.Name), editable: true) { Width = ControlDisplayInfo.DEFAULT_CONTROL_WIDTH_NORMAL },
new ControlDisplayInfoLight(nameof(IPolicy.Price), editable: true) { Width = ControlDisplayInfo.DEFAULT_CONTROL_WIDTH_NORMAL, Format = "n2" },
};
public override bool AutoOpenNewItemInTab => true;
public override bool AllowOpenCommand => true;
protected override void InitCommands(IList<ViewCommand> commands)
{
commands.Add(new SimpleHighestPolicyPriceIncrement(this));
base.InitCommands(commands);
}
}
You should be able to see no that in any of the LemonEdge clients (Windows Desktop Application, Admin Console, Web Browser, or Universal Core App) you can open the policies grid and increment the largest policy automatically, or select individual policies and increment them.
The individual ones won't be saved until you click save manually.
If you select no policy then the system will automatically save the increment as well. You won't see that immediately in the grid, you'll have to refresh it to see the change. As you'll see later we can of course, make the system refresh the grid to see the change immediately too. Which brings us on to our next steps, converting this algorithm into one that can run remotely on a task service.
Creating An Algorithm That Can Run As A Server Task
All algorithms can easily be upgraded to run as a task on a remote server.
When an algorithm has been integrated with server tasks, the system provides the following benefits:
- The task can be launched locally or on a task server
- The task can be launched from dedicated commands anywhere throughout the UI
- Whenever the task is launched the system can automatically provide a popup for any required parameters.
- The system will track the progress of the task if launched from the client application.
- All tasks are accessible from Server Tasks, and the state and progress can be viewed from there too
- The task can be created by creating a new Server Task and selecting a type of your new algorithm task. The system will allow the selection of any parameters before saving the task for processing.
- When a task is complete the system can view and open any results associated with the task
Note
When you create an algorithm various parts may be split across different dlls, depending on what you need the algorithm to do. For instance the parameters and algorithm itself will typically reside in your core addin dll. The result handler classes typically reside within the ui addin dll so the task can be launched, or results viewed, from the client application.
Server Task Algorithm Core Concepts
Before creating a task it is important to understand the following core concepts:
- Unique Task IDs
- Parameters
- Results
- ITaskProcessor Implementation
Unique IDs
Within the LemonEdge platform each algorithm that can run as a server task must be able to be uniquely identified. Each task type therefore has a unique Guid associated with it. This is used to refer to the type of task throughout the system.
When creating a task type you will need to generate your own unique Guid. You can do that using any method:
- CSharp:
Guid.NewGuid()
- SQL:
select NEWID()
- Online: Click here for generator
- etc...
We can then refer to that task id type using attributes against the task implementation, parameters and result handlers.
Creating Parameters
Parameters hold any custom parameters required for the task to operate correctly. Parameters must inherit from ServerTaskParameter.
To create a parameter for our policy algorithm, we are going to want a parameter that holds the id of a policy, and its name, that we want to run the algorithm against. We could create a custom parameter class with those serializable properties, or we can inherit ItemParameter which has an ID and ID_Label property already for us, like so:
[System.Runtime.Serialization.DataContract]
public class PolicyIncrementorTaskParameter : LemonEdge.API.Tasks.Parameters.ItemParameter
{
public const string UNIQUE_TASK_ID = "1f85223e-0bd8-4f1f-9bd3-7ae4f2c873f6";
public override string GetUserFriendlyTaskInstanceIdentityInfo(IServerTask task) =>
$"Policy Incrementor: {(string.IsNullOrEmpty(ID_Label) ? ID.ToString() : ID_Label)} ({task.Description})";
protected override SerializedParam CreateNewParam() => new PolicyIncrementorTaskParameter();
}
All we need for this parameter is a friendly label for the task itself while running, and the base class provides the properties we require. We would make sure this parameter is declared in our core addin dll so the algorithm can use it and the UI for showing and allowing the user to select those parameters.
Tip
If the actual task this parameter is for is only accessible on the server, and not the client ui and task service as normal, then this parameter also needs to be marked with the ServerTasksParameter attribute.
Creating Results
Results hold any custom information about the completion of the task itself. For instance data export tasks hold the file location of formatted data so the user can download and view it.
Results must inherit from ServerTaskResult. In this example, although we can theoretically obtain it from the parameter itself, we'll create our own results class that holds the id of the policy that we updated by running our algorithm:
[System.Runtime.Serialization.DataContract]
public class PolicyIncrementorTaskResult : LemonEdge.API.Tasks.ServerTaskResult, LemonEdge.Utils.Interfaces.ICloneable<PolicyIncrementorTaskResult>
{
[System.Runtime.Serialization.DataMember]
private Guid _policyID;
public Guid PolicyID { get => _policyID; set => _policyID = value; }
protected override SerializedParam CreateNewParam() => new PolicyIncrementorTaskResult();
#region ICloneable Implementation
public new PolicyIncrementorTaskResult Clone() => (PolicyIncrementorTaskResult)base.Clone();
public void CopyFromSource(PolicyIncrementorTaskResult source) => CopyFromParam(source);
protected override void CopyFromParam(SerializedParam source)
{
base.CopyFromParam(source);
var sourceParam = (PolicyIncrementorTaskResult)source;
_policyID = sourceParam.PolicyID;
}
#endregion
}
ITaskProcessor Implementation
To complete an algorithm that can run as a server task, we need an implementation of ITaskProcessor. Essentially a simple implementation of ProcessTask is all that's required.
Any implementation of this interface needs to be marked with the SererTask attribute, so the system understands how to execute this task.
Once you have a parameter, result class and implementation of this interface then everything is wired up for your task to be remotely executable on the task servers. Depending on your tasks requirements you may also need to wire up a parameters controller, and result handler on the ui side too.
Creating Our Algorithm
Now we have a parameter and result class we can modify our simple PolicyIncrementor to be able to run remotely as a task service, like so:
[LemonEdge.API.Tasks.ServerTask("Policy Incrementor", Parameters.PolicyIncrementorTaskParameter.UNIQUE_TASK_ID, typeof(Parameters.PolicyIncrementorTaskParameter),
Description = "Runs the policy incrementor algorithm against a specified policy.")]
public class PolicyIncrementorTask : ITaskProcessor
{
private IEntityUpdater _updater;
private IReadOnlyCache _cache;
private UserInfo _user;
private Parameters.PolicyIncrementorTaskParameter _params;
public IEntityUpdater Updater => _updater;
public IReadOnlyCache Cache => _cache;
public UserInfo User => _user;
public BusyProgressReporter Reporter { get; set; }
public Task Init(ITaskRunner runner, ServerTaskParameter ProcessTaskParameter)
{
(_updater, _cache, _user) = (runner.Updater, runner.Cache, runner.User);
_params = (Parameters.PolicyIncrementorTaskParameter)ProcessTaskParameter ?? throw new ArgumentNullException(nameof(ProcessTaskParameter));
return Task.FromResult(1);
}
public async Task<object> ProcessTask(CancellationToken cancel)
{
Reporter?.ReportProgress(1, "Parsing Parameters"); //Log process
Reporter?.ReportProgress(5, "Loading Policy...");
var policy = await Updater.GetItemByID<Interfaces.IPolicy>(_params.ID);
if(policy == null)
throw new ArgumentNullException($"Unable to find specified {_params.ID_Label} policy (id: {_params.ID}).");
Reporter?.ReportProgress(15, "Incrementing Policy '" + policy.Name + "' above highest current price...");
var calculator = new PolicyIncrementor(Updater, policy);
await calculator.IncrementAboveHighestPrice();
Updater.LogChange(policy, EntityOperation.Update);
Reporter?.ReportProgress(50, "Saving...");
await Updater.SaveChanges(cancel); //If not done the task runner will save any pending changes in the task Updater anyway
Reporter?.ReportProgress(90, "Complete");
///We can return just the policy id, but then we would need to implement a <see cref="LemonEdge.API.Tasks.ITaskResultHandler">ITaskResultHandler</see>
///which would take the policy id and return a LemonEdge.API.Tasks.ServerTaskResult anyway.
return new Results.PolicyIncrementorTaskResult() { PolicyID = policy.ID };
}
#region Dispose Implementation
private bool disposedValue;
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
// TODO: dispose managed state (managed objects)
}
// TODO: free unmanaged resources (unmanaged objects) and override finalizer
// TODO: set large fields to null
disposedValue = true;
}
}
// // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
// ~PolicyIncrementorTask()
// {
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
// Dispose(disposing: false);
// }
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
#endregion
}
If we go through this step by step, the first important part is the ServerTask Attribute:
[LemonEdge.API.Tasks.ServerTask("Policy Incrementor", Parameters.PolicyIncrementorTaskParameter.UNIQUE_TASK_ID, typeof(Parameters.PolicyIncrementorTaskParameter),
Description = "Runs the policy incrementor algorithm against a specified policy.")]
This simply tells the system the unique id for the type of task this algorithm implements along with the class that holds its parameters, and a user friendly description.
Next we essentially have the Initialization implementation of the task:
private IEntityUpdater _updater;
private IReadOnlyCache _cache;
private UserInfo _user;
private Parameters.PolicyIncrementorTaskParameter _params;
public IEntityUpdater Updater => _updater;
public IReadOnlyCache Cache => _cache;
public UserInfo User => _user;
public BusyProgressReporter Reporter { get; set; }
public Task Init(ITaskRunner runner, ServerTaskParameter ProcessTaskParameter)
{
(_updater, _cache, _user) = (runner.Updater, runner.Cache, runner.User);
_params = (Parameters.PolicyIncrementorTaskParameter)ProcessTaskParameter ?? throw new ArgumentNullException(nameof(ProcessTaskParameter));
return Task.FromResult(1);
}
Any algorithm that you would want to create within LemonEdge will only ever need access to an IEntityUpdate context, and optionally the current user, a local cache (for performance only you can still load the data from the context itself), and a method for reporting progress. Your algorithms are easy to adapt to utilise these classes ensuring most of your core logic is still re-usable.
In this Init implementation we are simply storing local copies of the context, user, cache and the parameters for running this task. The method returns a task incase you want to perform any loading/caching logic here too.
Next we actually implement our algorithm logic, and this case we've simply adapted our PolicyIncrementor logic to be used from within this class itself:
public async Task<object> ProcessTask(CancellationToken cancel)
{
Reporter?.ReportProgress(1, "Parsing Parameters"); //Log process
Reporter?.ReportProgress(5, "Loading Policy...");
var policy = await Updater.GetItemByID<Interfaces.IPolicy>(_params.ID);
if(policy == null)
throw new ArgumentNullException($"Unable to find specified {_params.ID_Label} policy (id: {_params.ID}).");
Reporter?.ReportProgress(15, "Incrementing Policy '" + policy.Name + "' above highest current price...");
var calculator = new PolicyIncrementor(Updater, policy);
await calculator.IncrementAboveHighestPrice();
Updater.LogChange(policy, EntityOperation.Update);
Reporter?.ReportProgress(50, "Saving...");
await Updater.SaveChanges(cancel); //If not done the task runner will save any pending changes in the task Updater anyway
Reporter?.ReportProgress(90, "Complete");
///We can return just the policy id, but then we would need to implement a <see cref="LemonEdge.API.Tasks.ITaskResultHandler">ITaskResultHandler</see>
///which would take the policy id and return a LemonEdge.API.Tasks.ServerTaskResult anyway.
return new Results.PolicyIncrementorTaskResult() { PolicyID = policy.ID };
}
That's it!
All we've really done is added a few lines of code to report the progress of the task, and to load the policy we want to change from the parameters of the task instead.
The key point here is we haven't had to alter the algorithm itself structurally, or in any other complicated fashion, to get it to run remotely on our task service. You are free to have your custom proprietary processes easily running on the client application or on task services with virtually no changes to your actual algorithm to achieve that.
Tip
Although we returned our result class, we could return just the policy id from the task itself, or any other form of raw unstructured data. We would then have to implement an ITaskResultHandler implementation that would be responsible for taking that raw data and transforming it into a ServerTaskResult.
Why do this? As sometimes the volume of data is large and you may want to have it stored, or be treated differently, if the task is actually just running locally on the client. In that case a custom IClientTaskResultHandler would take precedent and can just handle the raw unstructured data as is and process that straight on the client without it having to be saved to the ServerTask instance itself.
Creating Our Parameter View
Whenever a user creates a Server Task running our new algorithm, the system needs to understand how to present the parameters to the user.
To do this we have a standard ParamSingleViewerController you can inherit in order to specify the controls you want for all your properties on the parameter class. This works in exactly the same way as the controls available in the standard single view class (see that article for more info). We can specify that our ID property for our parameter should be presented to the user as a selection of policies by creating a ui controller for the parameter like so:
public class PolicyIncrementorParams : ParamSingleViewerController<Policy.Core.Tasks.Parameters.PolicyIncrementorTaskParameter>
{
public PolicyIncrementorParams(IParamSingleViewer<PolicyIncrementorTaskParameter> window, PolicyIncrementorTaskParameter param) : base(window, param)
{
}
protected override IEnumerable<ControlDisplayInfoLight> ControlNames() =>
new ControlDisplayInfoLight[]
{
new ControlDisplayInfoLight(nameof(PolicyIncrementorTaskParameter.ID), true) { UserFriendlyColumnName = "Policy" },
};
protected override void AlterControlInfo(ControlDisplayInfo info)
{
base.AlterControlInfo(info);
var type = LemonEdge.Core.Descriptors.EntityDescriptorFactory.GetDescriptor(typeof(Policy.Core.Interfaces.IPolicy));
var rel = new EntityRelationship(null, type, nameof(Policy.Core.Interfaces.IPolicy.AccountID), nameof(LemonEdge.API.Core.IBaseEntity.ID), LemonEdge.Utils.Database.SingleJoinType.One, "Policy", "Params");
switch (info.ColInfo.PropertyName)
{
case nameof(PolicyIncrementorTaskParameter.ID):
info.ColInfo.LabelPropertyName = nameof(PolicyIncrementorTaskParameter.ID) + ColumnDescriptor.LabelPropertyAppender;
info.Relationship = rel;
break;
default: throw new ArgumentOutOfRangeException(nameof(info.ColInfo.PropertyName));
}
}
}
The key part, just like in the single view controller, is specifying the controls we want and any customisation for them. Here we're specifying a control to be mapped to the ID property, and that it should hold a relationship to all policies in the system. This enables a control that allows any policy to be selected for the user.
See the ui/view articles for more documentation for customising these controllers.
Tip
Again - that's it. The system can now present parameters for your task whenever a user looks at any ServerTask that was running your algorithm, or when it is automatically created from a custom command integrated into the client ui. This works across the windows app, admin console, web browser and all other LemonEdge client applications.
Creating Our Result Handler
Depending on what your task does, sometimes you want the user to be able to interact with, or view, the results of your task. This is done by our result handlers.
As we updated a policy entity, our result holds the ID of the policy that is updated. All we need to do is implement the IClientTaskResultHandler so we can display the changed policy. Ideally we want to open the policy, or if it is already opened (and not changed) refresh it to load in any changes and select it.
We can implement that ourselves through our UI API and the IClientTaskResultHandler implementation, or we can simply inherit from the base OpenOrRefreshResultItem default client task handler, that will perform all those functions for us. Like so:
[LemonEdge.API.Tasks.ServerTaskResult(Policy.Core.Tasks.Parameters.PolicyIncrementorTaskParameter.UNIQUE_TASK_ID, ServerTaskResultType.Client)]
public class PolicyIncrementorResultHandler : OpenOrRefreshResultItem<Policy.Core.Tasks.Results.PolicyIncrementorTaskResult>
{
protected override Task<PolicyIncrementorTaskResult> ConvertRawResultIntoResultType(ServerTaskParameter parameter, object rawResult) => Task.FromResult((PolicyIncrementorTaskResult)rawResult);
protected override (Guid TypeID, Guid ID) GetItemToOpenOrRefresh(PolicyIncrementorTaskResult resultInfo) =>
(LemonEdge.Core.Descriptors.EntityDescriptorFactory.GetDescriptor(typeof(Policy.Core.Interfaces.IPolicy)).TypeID, resultInfo.PolicyID);
}
Tip
See the TaskHandlers article for more information and the differences between client and server handlers.
We can see here though that all we need to do to handle the task is mark our handler with the ServerTaskResult attribute indicating the task this handles results for. Because our base class does all the work, we simply only need to say the type and id of the item we would like to open/refresh from the supplied result class.
Creating A Command To Run Our Task Automatically
When we have an algorithm defined that can run as a task, the user can always initiate that task by creating a new Server Task, selecting the task type, entering any parameters and executing it immediately or on a schedule. This is extremely useful from an administration perspective, and also means users can see the history of all tasks run, their progress/log, and their results too.
However you'll often want to run the task from elsewhere in the UI for the user. You can of course create a ServerTask, with all the right parameters, yourself in any command and save it. This will result in the task server picking it up and processing it as soon as it can. You can of course do this from any menu command, view command, or custom process you wish to fully customise the flexibility of launching your task.
We also provide a TaskBase view command that with a few settings can automatically be configured to perform all of the following for you:
- Launch your task locally to be processed on the client, or on a server by a task service
- Prompt the user for any parameters
- Automatically track the progress of the task (either running locally or on the server)
- Display any errors
- Process the results of the task automatically upon completion
To do that, we'd like to add the ability to launch our task from the view of a policy. This way a user can open a policy, click the simple policy increment we created earlier, or click the Task Policy Increment that will launch the process as a task on the server. Like so:
public class TaskPolicyIncrement : LemonEdge.Client.Core.Commands.ViewCommands.TaskBase
{
private readonly Func<IPolicy> _getPolicy;
public TaskPolicyIncrement(PolicyController controller) :
base(
runOnServer: true,
controller.View.Displayer,
Guid.Parse(Policy.Core.Tasks.Parameters.PolicyIncrementorTaskParameter.UNIQUE_TASK_ID)
) => _getPolicy = () => controller.SingleItem;
public override bool InternalCanExecute(object parameter) => true;
protected override Task<ServerTaskParameter> GetParameters()
{
var policy = _getPolicy();
ServerTaskParameter result = new Policy.Core.Tasks.Parameters.PolicyIncrementorTaskParameter() { ID = policy.ID, ID_Label = policy.Name };
return Task.FromResult(result);
}
}
You can see here we're simply informing the base TaskBase command we want the task to run on the server and the type of task to run. We also provide the parameters hard coded to the policy the view is showing so we don't need to prompt for any parameters.
We can then add this as a view command to our policy view like so:
protected override void InitCommands(IList<ViewCommand> commands)
{
base.InitCommands(commands);
commands.Add(new TaskPolicyIncrement(this));
commands.Add(LemonEdge.Client.Core.Commands.ViewCommands.ViewCommandSeperator.Seperator);
commands.Add(new SimplePolicyPriceIncrement(this));
}
That's it!
When the user clicks this button it will automatically launch the task on the server for you and display the tracking of the progress of that task at the same time. As soon as the task has completed the system will launch the result handler and refresh (or open if the policy has been closed) the policy to show the change made by the task on the server.
Note
The user can also go to the Server Tasks list and see the task that was run, view the parameters (being the policy we ran this for) and view the result (again the same parameter) at any point in time in the future, providing a complete log of all the activity of this task.
Next Steps
Creating an algorithm on LemonEdge is incredibly simple and at it's heart only really involves your algorithm referencing the IEntityUpdater context, which allows you to query all the data in the entire system, all of their history, any custom query, and of course the ability to update, delete and insert data too. By referencing your entity interfaces your algorithm can be as portable as you want to make it.
Important
Most importantly though, as you gain the benefit of integrating your algorithms onto the LemonEdge platform you gain the immediate benefit of straight-through processing and the ability to tightly knit your processes together in a way that is simply not possible when processes exist outside your system. For instance your algorithm could calculate transactional data, and then because it's running in the platform could automatically create those transactions as part of the same process.
Lastly, without you having to modify your algorithm in any way, you also immediately benefit from the following platform features:
- Complete history of all executions of your algorithm along with parameters and results
- Full auditing of any changes your algorithm makes to any data
- Integrated reporting across all the changed entities
- The ability for your algorithm to be run "as-of" any prior point in time
- The ability for you algorithm to run seamlessly, and isolated, in different canvases.
These Getting Started articles take you through the common requirements for creating entities, associated ui views, commands, and of course custom algorithms. Together this typically represents 80% of the functionality you're going to want to implement when starting out with a LemonEdge development.
There are a set of more advanced articles available as well covering more specific topics. Ultimately over 60% of the LemonEdge product itself is built using our own API, including functionality such as the Financial Services Engine. Using our API you can build your financial services functionality out, or tailor existing products, to 100% of your needs 80% faster than traditional legacy methods.