cancel
Showing results for 
Search instead for 
Did you mean: 
cchannon

PowerFX Plugins: Part 2: The Plugin

cchannon_0-1670356602731.png PowerFX Plugins

Recently, I started a journey with the PowerFX language as a Pro-Coder. The objective: to enable low-coders to author Plugin executions in PowerFX instead of with C#, finally opening up the power of event-driven logic to the broader Power Platform community.

 

This is Part 2 of a series of blog posts I am writing about that Journey - and it is pretty much the beating heart of the solution: A Plugin Package that includes the latest prerelease PowerFX Interpreter code so that at runtime we can grab PowerFX authored for the event, evaluate it, and commit changes to the record mid-transaction. You can find part 1 here on the Community Blogs too, where I detailed how to create a Monaco code editor for PowerFX authoring.

Scoping the effort

OK, so for a topic as big as "I want to execute PowerFX in a plugin" it was necessary for me to downscope the effort and put some limits on it to put out a quick prototype that I (and hopefully others in the community!) can continue to build on once the pattern has been proven. So, I put some constraints on the definition.

  • First, I decided to start by just looking at an Update event, and no other plugin events where I might have a non-entity Target or there might not be a Pre or a Post image. 
  • Second, I decided that the PowerFX mutation functions would only have access to the current executing record, and no others. I think it will be very important in the future to be able to define Data Sources and use behavior and non-behavior functions on the entire Dataverse database, but for this prototype, I left is scoped to exclusively the current record (kind of like a Business Rule).
  • Third, I decided to begin with supporting only 'standard' columns and not your more complex multi-select optionsets, attachments, etc.

OK, so with a narrowly defined scope for the plugin support, let's get right into it.

The Code

(tldr; to jump straight to the source code, ready-to-compile, you can grab it from my GitHub repo here.)

OK, so to begin with, I used the PAC plugin template for this because I needed to include PowerFX assembly dependencies, so this project is a Plugin Package not a Plugin Assembly. If you're not familiar with this, you can read up on it here. This capability, like much of what I'm using in this project, is technically still in Preview so use any of this stuff only at your own risk!

 

The first thing we're going to do is declare some global variables for convenience and read the Plugin Step Unsecure Config to look for a record Id. This record Id is the guid of the record that will store our PowerFX string (where we registered the Monaco Editor from Part 1 of this blog):

 

 

 

private string pfxRecordId = null;
private ILocalPluginContext _pluginContext;
private ParserOptions opts;
private RecalcEngine engine;
private List<StartMap> preExecutionValues = new List<StartMap>();
AttributeMetadata[] attributeMetadata;
private readonly List<string> reservedColumns = new List<string>(){"createdon","overriddencreatedon","createdby","modifiedon","modifiedby","timezoneruleversionnumber","versionnumber","importsequencenumber","utcconversiontimezonecode"};
private List<RecordValue> references = new List<RecordValue>();

public Plugin1(string unsecureConfiguration, string secureConfiguration): base(typeof(Plugin1))
{
    if(unsecureConfiguration != null){
        pfxRecordId = unsecureConfiguration;
    }
}

 

 

 

*Note the var reservedColumns. We're declaring that up top so it is convenient to edit in the future; the idea here is that there are columns that we don't want to let users even try to update. This List stores those names so we know at runtime no matter what the user puts in their PFX, we're going to totally ignore any attempt to Set a value into these columns.

 

OK, now down in the execute method we'll start doing good stuff like instantiating our Engine. The Engine is a PFX Interpreter concept and it is the magic that makes it all work. This object gives us the ability to declare variables, evaluate PFX strings, and more. In a later blog post I will go into more detail on what all these lines are doing and why we're setting up the Interpreter this way, but for now, just know this is the way to do it if you want to support mutations (non-behavior functions).

 

 

 

var config = new PowerFxConfig();
config.EnableSetFunction();
opts = new ParserOptions { AllowsSideEffects = true };

engine = new RecalcEngine(config);
var symbol = new SymbolTable();
symbol.EnableMutationFunctions();
engine.Config.SymbolTable = symbol;

 

 

 

Now we have an engine, but before we can evaluate the PFX, we need to declare all our variables and given them values. (i.e. the engine needs to know that new_name is a string and it has a certain value). This is a bit tricky to do because we will have three different kinds of variable/value:

  • Attributes in the plugin execution Target for Update (i.e. the "new" value being applied by the update)
  • Attributes in the Record PreImage (i.e. all the attribute values on the record that are NOT in the update)
  • Attributes that are null on the record and null in the Target, but we still want to know they exist.

So we're going to get that data from three places, the Plugin "Target", a PreEntityImage called PreImage (that contains all columns) and a Metadata call to the API. We will declare each of them as its own Variable, which is a little different from how you might access record attributes in Canvas Apps (usually, a Dataverse Record is a List of Records/Variables) but since we've already specified that our plugin will only be aware of the one executing record, it seems just simpler to let the user simple give a column schemaname to access that column, rather than accessing it as a member of a list.

 

 

 

if (_pluginContext.PluginExecutionContext.InputParameters["Target"] is Entity target)
{
    _pluginContext.Trace("Have Target");
    
    Entity preImage = !_pluginContext.PluginExecutionContext.PreEntityImages.Contains("PreImage") ? throw new InvalidPluginExecutionException("No PreImage with the name PreImage was found registered on this Plugin Command. Please check the step registration and correctly register the image with all attributes.") : _pluginContext.PluginExecutionContext.PreEntityImages["PreImage"];

    var entityMetadataRequest = new RetrieveEntityRequest
    {
        EntityFilters = EntityFilters.Attributes,
        LogicalName = target.LogicalName
    };

    EntityMetadata metadataResponse = (EntityMetadata)orgService.Execute(entityMetadataRequest).Results.FirstOrDefault().Value;

    attributeMetadata = metadataResponse.Attributes;
    
    _pluginContext.Trace("Declaring Variables");
    foreach (var attrib in attributeMetadata)
    {
        //only evaluate valid, non-virtual and non-calcluated cols: 
        if (attrib.IsValidODataAttribute)
        {
            _pluginContext.Trace($"attr: {attrib.LogicalName}");
            Entity source = null;
            if (target.Attributes.ContainsKey(attrib.LogicalName))
                source = target;
            else if (preImage.Attributes.ContainsKey(attrib.LogicalName))
                source = preImage;

            DeclareVariable(source, attrib.LogicalName);
        }
    }
    engine.UpdateVariable("Lookups", FormulaValue.NewTable(references.FirstOrDefault().Type, references));

 

 

 

Now we're ready to eval the PFX. We'll Retrieve the record where the pfx is stored (in my environment, I called the table ktcs_plugincommand) and then evaluate the string, printing to the plugin trace any output from the evaluation

 

 

var query = new QueryExpression("ktcs_plugincommand");
query.ColumnSet.AddColumns(new string[] { "ktcs_command", "ktcs_formulas", "ktcs_functions" });
query.Criteria.AddCondition(new ConditionExpression("ktcs_plugincommandid", ConditionOperator.Equal, pfxRecordId));
var results = orgService.RetrieveMultiple(query).Entities;

if (results != null && results.Count > 0)
{
    var pfxstring = results[0].GetAttributeValue<string>("ktcs_command");
    string[] lines = pfxstring.Split(';');
    _pluginContext.Trace($"{lines.Count()} commands found.");
    foreach (var line in lines)
    {
        if (line == "")
            continue;
        var result = engine.Eval(line, null, opts);

        if (result is ErrorValue errorValue)
            throw new InvalidPluginExecutionException("Error in PowerFX Evaluation: " + errorValue.Errors[0].Message);
        else
        {
            _pluginContext.Trace($"Non-Behavior Output: {PrintResult(result)}");
        }
    }

 

 

Now we're ready to handle the attribute mutations. We're not sure what the PFX might have updated, so we'll just look over our list of attributes and compare the value they had originally with the value that would now be output by the Engine. If we find any differences, we'll add that new value to Target:

 

 

var query = new QueryExpression("ktcs_plugincommand");
query.ColumnSet.AddColumns(new string[] { "ktcs_command", "ktcs_formulas", "ktcs_functions" });
query.Criteria.AddCondition(new ConditionExpression("ktcs_plugincommandid", ConditionOperator.Equal, pfxRecordId));
var results = orgService.RetrieveMultiple(query).Entities;

if (results != null && results.Count > 0)
{
    var pfxstring = results[0].GetAttributeValue<string>("ktcs_command");
    string[] lines = pfxstring.Split(';');
    _pluginContext.Trace($"{lines.Count()} commands found.");
    foreach (var line in lines)
    {
        if (line == "")
            continue;
        var result = engine.Eval(line, null, opts);

        if (result is ErrorValue errorValue)
            throw new InvalidPluginExecutionException("Error in PowerFX Evaluation: " + errorValue.Errors[0].Message);
        else
        {
            _pluginContext.Trace($"Non-Behavior Output: {PrintResult(result)}");
        }
    }

    //inject updates into Target
    AttributeCollection update = CompareContext();
    if (update != null && update.Count() > 0)
    {
        foreach(var attrib in update)
        {
            _pluginContext.Trace($"{attrib.Key} : {attrib.Value}");
            target[attrib.Key] = attrib.Value;
        }
    }

 

 

At a high level, that's pretty much it! There's more to it, including a whole ton of typecasting between Dataverse Entity types and PowerFX datatypes (which all come out as Object) but the procedure is overall pretty straightforward. In my next post on this I'll go into more detail on how all this works and why; a much deeper discussion of PowerFX and how to work with the Interpreter to build your own solutions!

 

Remember to checkout my GitHub repo for the full code to this plugin as well as other pieces of the overall PowerFX Plugin authoring experience!