0
votes

I would like to pass an object and expression into a dynamically created workflow to mimic the Eval function found in many languages. Can anyone help me out with what I am doing wrong? The code below is a very simple example if taking in a Policy object, multiple its premium by 1.05, then return the result. It throws the exception:

Additional information: The following errors were encountered while processing the workflow tree:

'DynamicActivity': The private implementation of activity '1: DynamicActivity' has the following validation error: Value for a required activity argument 'To' was not supplied.

And the code:

using System.Activities;
using System.Activities.Statements;

namespace ConsoleApplication1
{
  class Program
  {
    static void Main(string[] args)
    {
        Policy p = new Policy() { Premium = 100, Year = 2016 };

        var inputPolicy = new InArgument<Policy>();
        var theOutput = new OutArgument<object>();

        Activity dynamicWorkflow = new DynamicActivity()
        {
            Properties =
            {
                new DynamicActivityProperty
                {
                    Name="Policy",
                    Type=typeof(InArgument<Policy>),
                    Value=inputPolicy
                }
            },
            Implementation = () => new Sequence()
            {
                Activities =
                {
                    new Assign()
                    {
                         To =  theOutput,
                         Value=new InArgument<string>() { Expression = "Policy.Premium * 1.05" }
                    }
                }
            }
        };

        WorkflowInvoker.Invoke(dynamicWorkflow);
    }
}

  public class Policy
  {
      public int Premium { get; set; }
      public int Year { get; set; }
  }
}
1
Just curious if you have to use WF in general. There is a NuGet library called NCalc that allows you to evaluate expressions, which definitely can do dynamic math formulas. ncalc.codeplex.com - ajawad987
I did research NCalc - we are already using workflow extensively in our product, and some of the expressions we use are quite complex. Also Wf is built by MS so I know it is quite robust in its capabilities. Thanks for the suggestion though! - John Hennesey
Don't use edits like your last to bump posts please; adding noise to a post is not going to get you the help you need. See How do I get attention for one of my own questions without a good answer?, if you are going to edit your post, use it to improve it, not just say 'bump, can anyone help me?'. - Martijn Pieters

1 Answers

1
votes

You can use Workflow Foundation to evaluate expressions, but it is far easier to use almost any other option.

The key issue at play with your code was that you were not trying to evaluate the expression (with either VisualBasicValue or CSharpValue). Assigning InArgument`1.Expression is an attempt to set the value - not to set the value to the result of an expression.

Keep in mind that compiling expressions is fairly slow (>10ms), but the resultant compiled expression can be cached for quick executions.

Using Workflow:

class Program
{
    static void Main(string[] args)
    {
        // this is slow, only do this once per expression
        var evaluator = new PolicyExpressionEvaluator("Policy.Premium * 1.05");

        // this is fairly fast

        var policy1 = new Policy() { Premium = 100, Year = 2016 };
        var result1 = evaluator.Evaluate(policy1);

        var policy2 = new Policy() { Premium = 150, Year = 2016 };
        var result2 = evaluator.Evaluate(policy2);

        Console.WriteLine($"Policy 1: {result1}, Policy 2: {result2}");
    }

}

public class Policy
{
    public double Premium, Year;
}

class PolicyExpressionEvaluator
{
    const string 
        ParamName = "Policy",
        ResultName = "result";

    public PolicyExpressionEvaluator(string expression)
    {
        var paramVariable = new Variable<Policy>(ParamName);
        var resultVariable = new Variable<double>(ResultName);
        var daRoot = new DynamicActivity()
        {
            Name = "DemoExpressionActivity",
            Properties =
            {
                new DynamicActivityProperty() { Name = ParamName, Type = typeof(InArgument<Policy>) },
                new DynamicActivityProperty() { Name = ResultName, Type = typeof(OutArgument<double>) }
            },
            Implementation = () => new Assign<double>()
            {
                To = new ArgumentReference<double>() { ArgumentName = ResultName },
                Value = new InArgument<double>(new CSharpValue<double>(expression))
            }
        };
        CSharpExpressionTools.CompileExpressions(daRoot, typeof(Policy).Assembly);
        this.Activity = daRoot;
    }

    public DynamicActivity Activity { get; }

    public double Evaluate(Policy p)
    {
        var results = WorkflowInvoker.Invoke(this.Activity, 
            new Dictionary<string, object>() { { ParamName, p } });

        return (double)results[ResultName];
    }
}

internal static class CSharpExpressionTools
{
    public static void CompileExpressions(DynamicActivity dynamicActivity, params Assembly[] references)
    {
        // See https://docs.microsoft.com/en-us/dotnet/framework/windows-workflow-foundation/csharp-expressions
        string activityName = dynamicActivity.Name;
        string activityType = activityName.Split('.').Last() + "_CompiledExpressionRoot";
        string activityNamespace = string.Join(".", activityName.Split('.').Reverse().Skip(1).Reverse());
        TextExpressionCompilerSettings settings = new TextExpressionCompilerSettings
        {
            Activity = dynamicActivity,
            Language = "C#",
            ActivityName = activityType,
            ActivityNamespace = activityNamespace,
            RootNamespace = null,
            GenerateAsPartialClass = false,
            AlwaysGenerateSource = true,
            ForImplementation = true
        };

        // add assembly references
        TextExpression.SetReferencesForImplementation(dynamicActivity, references.Select(a => (AssemblyReference)a).ToList());

        // Compile the C# expression.  
        var results = new TextExpressionCompiler(settings).Compile();
        if (results.HasErrors)
        {
            throw new Exception("Compilation failed.");
        }

        // attach compilation result to live activity
        var compiledExpression = (ICompiledExpressionRoot)Activator.CreateInstance(results.ResultType, new object[] { dynamicActivity });
        CompiledExpressionInvoker.SetCompiledExpressionRootForImplementation(dynamicActivity, compiledExpression);
    }
}

Compare to the equivalent Roslyn code - most of which is fluff that is not really needed:

public class PolicyEvaluatorGlobals
{
    public Policy Policy { get; }

    public PolicyEvaluatorGlobals(Policy p)
    {
        this.Policy = p;
    }
}

internal class PolicyExpressionEvaluator
{
    private readonly ScriptRunner<double> EvaluateInternal;

    public PolicyExpressionEvaluator(string expression)
    {
        var usings = new[] 
        {
            "System",
            "System.Collections.Generic",
            "System.Linq",
            "System.Threading.Tasks"
        };
        var references = AppDomain.CurrentDomain.GetAssemblies()
            .Where(a => !a.IsDynamic && !string.IsNullOrWhiteSpace(a.Location))
            .ToArray();

        var options = ScriptOptions.Default
            .AddImports(usings)
            .AddReferences(references);

        this.EvaluateInternal = CSharpScript.Create<double>(expression, options, globalsType: typeof(PolicyEvaluatorGlobals))
            .CreateDelegate();
    }

    internal double Evaluate(Policy policy)
    {
        return EvaluateInternal(new PolicyEvaluatorGlobals(policy)).Result;
    }
}

Roslyn is fully documented, and has the helpful Scripting API Samples page with examples.