WF4:如何计算仅在运行时已知的表达式

本文关键字:运行时 表达式 何计算 计算 WF4 | 更新日期: 2023-09-27 18:32:10

我正在尝试创建一个简单的 WF4 活动,该活动接受包含 VB.NET 表达式(来自数据库)的字符串,使用工作流当前范围内可用的变量计算该字符串并返回结果。不幸的是,以我尝试过的方式,无论是平原Activity还是成熟的NativeActivity,我不断碰壁。

我的第一次尝试是使用一个简单的 Activity,我能够创建一个简单的类来计算给定某个对象作为其输入的表达式:

public class Eval<T, TResult> : Activity<TResult>
{
    [RequiredArgument]
    public InArgument<T> Value { get; set; }
    public Eval(string predicate)
    {
        this.Implementation = () => new Assign<TResult>
        {
            Value = new InArgument<TResult>(new VisualBasicValue<TResult>(predicate)),
            To = new ArgumentReference<TResult>("Result")
        };
    }
    public TResult EvalWith(T value)
    {
        return WorkflowInvoker.Invoke(this, new Dictionary<string, object>{ {"Value", value } });
    }
}

这很好,以下表达式的计算结果为 7:

new Eval<int, int>("Value + 2").EvalWith(5)

不幸的是,我不能按照我想要的方式使用它,因为表达式字符串是作为构造函数参数而不是InArgument<string>给出的,因此它不能轻易合并(拖放)到工作流程中。我的第二次尝试是尝试使用NativeActivity来摆脱那个讨厌的构造函数参数:

public class NativeEval<T, TResult> : NativeActivity<TResult>
{
    [RequiredArgument] public InArgument<string> ExpressionText { get; set; }
    [RequiredArgument] public InArgument<T> Value { get; set; }
    private Assign Assign { get; set; }
    private VisualBasicValue<TResult> Predicate { get; set; }
    private Variable<TResult> ResultVar { get; set; }
    protected override void CacheMetadata(NativeActivityMetadata metadata)
    {
        base.CacheMetadata(metadata);
        Predicate = new VisualBasicValue<TResult>();
        ResultVar = new Variable<TResult>("ResultVar");
        Assign = new Assign { To = new OutArgument<TResult>(ResultVar), Value = new InArgument<TResult>(Predicate) };
        metadata.AddVariable(ResultVar);
        metadata.AddChild(Assign);
    }
    protected override void Execute(NativeActivityContext context)
    {
        Predicate.ExpressionText = ExpressionText.Get(context);
        context.ScheduleActivity(Assign, new CompletionCallback(AssignComplete));
    }
    private void AssignComplete(NativeActivityContext context, ActivityInstance completedInstance)
    {
        Result.Set(context, ResultVar.Get(context));
    }
}

我尝试使用以下方法运行NativeEval

WorkflowInvoker.Invoke(new NativeEval<int, int>(), new Dictionary<string, object>
    { { "ExpressionText", "Value + 2" }, { "Value", 5 } });

但出现以下异常:

活动"1:NativeEval"无法访问此变量,因为它是在活动"1:NativeEval"的范围内声明的。 活动只能访问其自己的实现变量。

所以我metadata.AddVariable(ResultVar);更改为metadata.AddImplementationVariable(ResultVar);,但后来我得到了一个不同的例外:

处理工作流树时遇到以下错误: "变量引用":引用的变量对象(名称 = "结果变量")在此范围内不可见。 在此范围内可能可以看到另一个具有相同名称的位置引用,但它不引用相同的位置。

我尝试使用此处描述的.ScheduleFunc()来计划VisualBasicValue活动,但它返回的结果总是null(但奇怪的是没有抛出异常)。

我被难住了。WF4 的元编程模型似乎比 System.Linq.Expressions 的元编程模型困难得多,尽管这很困难且经常令人困惑(就像元编程通常一样),但至少我能够理解它。我想这是因为它增加了复杂性,需要表示一个持久的、可恢复的、异步的、可重定位的程序,而不仅仅是一个普通的旧程序。


编辑:由于我认为我遇到的问题不是由于我正在尝试评估未硬编码的表达式这一事实引起的,因此可以对导致其具有静态表达式的NativeActivity进行以下更改:

取代

Predicate = new VisualBasicValue<TResult>();

Predicate = new VisualBasicValue<TResult>("ExpressionText.Length");

并删除该行

Predicate.ExpressionText = ExpressionText.Get(context);

现在,即使这些行的表达式是静态的,我仍然遇到相同的错误。


编辑2:这篇文章解决了我得到的异常。我必须将变量和子活动都更改为"实现",因此:

metadata.AddVariable(ResultVar);
metadata.AddChild(Assign);

改为:

metadata.AddImplementationVariable(ResultVar);
metadata.AddImplementationChild(Assign);

并导致所有异常消失。不幸的是,它揭示了以下行绝对没有任何作用:

Predicate.ExpressionText = ExpressionText.Get(context);

在运行时更改VisualBasicValueExpressionText 属性不起作用。使用 ILSpy 的快速检查揭示了原因 - 表达式文本仅在调用CacheMetadata()时才被计算并转换为表达式树,此时表达式还不知道,这就是为什么我使用无参数构造函数将表达式初始化并结晶为无操作。我甚至尝试将我得到的NativeActivityMetadata对象保存在我自己的 CacheMetadata 覆盖方法中,然后使用反射强制调用VisualBasicValueCacheMetadata(),但这最终抛出了一个不同的神秘异常("找到模棱两可的匹配"类型为歧义匹配异常)。

此时,似乎不可能将动态表达式完全集成到工作流中,从而向它公开所有范围内的变量。我想我会在NativeEval类中使用Eval类中使用的方法。

WF4:如何计算仅在运行时已知的表达式

我最终使用了以下活动。它无法访问工作流的变量,而是接受单个参数"Value",该参数可在动态表达式中由相同名称使用。除此之外,它工作得很好。

public class Evaluate<TIn, TOut> : NativeActivity<TOut>
{
    [RequiredArgument]
    public InArgument<string> ExpressionText { get; set; }
    [RequiredArgument]
    public InArgument<TIn> Value { get; set; }
    protected override void Execute(NativeActivityContext context)
    {
        var result = new ExpressionEvaluator<TIn, TOut>(ExpressionText.Get(context)).EvalWith(Value.Get(context));
        Result.Set(context, result);
    }
}
public class ExpressionEvaluator<TIn, TOut> : Activity<TOut>
{
    [RequiredArgument]
    public InArgument<TIn> Value { get; set; }
    public ExpressionEvaluator(string predicate)
    {
        VisualBasic.SetSettingsForImplementation(this, VbSettings);
        Implementation = () => new Assign<TOut>
        {
            Value = new InArgument<TOut>(new VisualBasicValue<TOut>(predicate)),
            To = new ArgumentReference<TOut>("Result")
        };
    }
    public TOut EvalWith(TIn value)
    {
        return WorkflowInvoker.Invoke(this, new Dictionary<string, object> { { "Value", value } });
    }
    private static readonly VisualBasicSettings VbSettings;
    static ExpressionEvaluator()
    {
        VbSettings = new VisualBasicSettings();
        AddImports(typeof(TIn), VbSettings.ImportReferences);
        AddImports(typeof(TOut), VbSettings.ImportReferences);
    }
    private static void AddImports(Type type, ISet<VisualBasicImportReference> imports)
    {
        if (type.IsPrimitive || type == typeof(void) || type.Namespace == "System")
            return;
        var wasAdded = imports.Add(new VisualBasicImportReference { Assembly = type.Assembly.GetName().Name, Import = type.Namespace });
        if (!wasAdded)
            return;
        if (type.BaseType != null)
            AddImports(type.BaseType, imports); 
        foreach (var interfaceType in type.GetInterfaces())
            AddImports(interfaceType, imports);
        foreach (var property in type.GetProperties())
            AddImports(property.PropertyType, imports);
        foreach (var method in type.GetMethods())
        {
            AddImports(method.ReturnType, imports);
            foreach (var parameter in method.GetParameters())
                AddImports(parameter.ParameterType, imports);
            if (method.IsGenericMethod)
            {
                foreach (var genericArgument in method.GetGenericArguments())
                    AddImports(genericArgument, imports);
            }
        }
        if (type.IsGenericType)
        {
            foreach (var genericArgument in type.GetGenericArguments())
                AddImports(genericArgument, imports);
        }
    }
}

编辑:更新了类以包括完整的程序集和命名空间导入,以免您收到可怕的(和无用的)错误消息:

未声明"值"。由于其保护级别,它可能无法访问。

此外,将 ExpressionEvaluator 类移到外部并使其公开,以便可以在 WF 外部使用它,如下所示:

new ExpressionEvaluator<int, double>("Value * Math.PI").EvalWith(2);

这将返回:

6.28318530717959

我建议为此使用不同的框架。一个好的方法是使用nCalc。

http://ncalc.codeplex.com/它可以解析任何表达式并计算结果,包括静态或动态参数和自定义函数。

我们使用它在运行时评估不同类型的表达式。

如果你的"谓词"是一个众所周知的字符串,并且不需要是运行时计算的表达式,你当然可以做这样的事情,扔掉InArgument并避免构造函数:

public class Eval<T, TResult> : Activity<TResult>
{
    public string Expression { get; set; }
    [RequiredArgument]
    public InArgument<T> Value { get; set; }
    protected override Func<Activity> Implementation
    {
        get
        {
            if (string.IsNullOrEmpty(Expression))
            {
                return base.Implementation;
            }
            return () => new Assign<TResult>
            {
                Value = new InArgument<TResult>(new VisualBasicValue<TResult>(Expression)),
                To = new ArgumentReference<TResult>("Result")
            };
        }
        set
        {
            throw new NotSupportedException();
        }
    }
}

并这样称呼它:

var activity = new Eval<int, int>() { Expression = "Value + 2" };
var inputArgs = new Dictionary<string, object>()
{
    { "Value", 5 }
};
Console.WriteLine("RESULT: " + WorkflowInvoker.Invoke<int>(activity, inputArgs));

编辑:检查即使没有Predicate.ExpressionText注释,它也没有效果:

public class NativeEval<T, TResult> : NativeActivity<TResult>
{
    [RequiredArgument]
    public InArgument<string> ExpressionText { get; set; }
    [RequiredArgument]
    public InArgument<T> Value { get; set; }
    private Assign Assign { get; set; }
    private VisualBasicValue<TResult> Predicate { get; set; }
    private Variable<TResult> ResultVar { get; set; }
    protected override void CacheMetadata(NativeActivityMetadata metadata)
    {
        base.CacheMetadata(metadata);
        Predicate = new VisualBasicValue<TResult>("ExpressionText.Length");
        ResultVar = new Variable<TResult>("ResultVar");
        Assign = new Assign { To = new OutArgument<TResult>(ResultVar), Value = new InArgument<TResult>(Predicate) };
        metadata.AddImplementationVariable(ResultVar);
        metadata.AddImplementationChild(Assign);
    }
    protected override void Execute(NativeActivityContext context)
    {
        // this line, commented or not, is the same!
        Predicate.ExpressionText = ExpressionText.Get(context);
        context.ScheduleActivity(Assign, new CompletionCallback(AssignComplete));
    }
    private void AssignComplete(NativeActivityContext context, ActivityInstance completedInstance)
    {
        // the result will always be the ExpressionText.Length 
        Result.Set(context, ResultVar.Get(context));
    }
}

当您获得Execute()方法时,更改子实现不起作用。执行模式处于打开状态,无法更改子树。