Use workflow to evaluate dynamic expression

2019-03-06 03:16发布

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条回答
我只想做你的唯一
2楼-- · 2019-03-06 03:52

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.

查看更多
登录 后发表回答