WF4:如何评估仅在运行时已知的表达式?

时间:2012-04-23 16:44:00

标签: c# .net workflow-foundation-4

我正在尝试创建一个简单的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:<​​/ p>

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 } });

但得到以下例外:

  

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

所以我将metadata.AddVariable(ResultVar);更改为metadata.AddImplementationVariable(ResultVar);,但后来又有了一个例外:

  

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

我尝试使用.ScheduleFunc()描述VisualBasicValue来安排null活动,但返回的结果始终是System.Linq.Expressions(但奇怪的是没有抛出异常)。

我很难过。 WF4的元编程模型似乎比NativeActivity的元编程模型困难得多,虽然这种模型虽然困难且经常令人困惑(通常是元编程),但至少我能够绕过它。我想这是因为它需要代表一个可持久的,可恢复的,异步的,可重定位的程序,而不仅仅是一个普通的旧程序,这增加了复杂性。


编辑:由于我不认为我遇到的问题是由于我正在尝试评估一个非硬编码的表达式,因此可以进行以下更改导致它具有静态表达式的Predicate = new VisualBasicValue<TResult>();

替换

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

使用

Predicate.ExpressionText = ExpressionText.Get(context);

删除该行

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

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


EDIT2 here解决了我遇到的异常问题。我不得不将变量和子活动都改为“实现”,所以这个:

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

改为:

Predicate.ExpressionText = ExpressionText.Get(context);

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

ExpressionText

在运行时更改VisualBasicValue的{​​{1}}属性无效。使用ILSpy进行快速检查可以发现原因 - 只调用表达式文本并在调用CacheMetadata()时将其转换为表达式树,此时表达式尚不清楚,这就是我使用无参数构造函数初始化和将表达式结晶为无操作。我甚至尝试保存我在自己的CacheMetadata重写方法中获得的NativeActivityMetadata对象,然后使用反射强制调用VisualBasicValue的{​​{1}},但这最终导致了一个不同的神秘异常(“Ambiguous match found。”,类型为AmbiguousMatchException)。

此时似乎无法将动态表达式完全集成到工作流中,从而将所有范围内变量公开给它。我想我会在CacheMetadata()课程中的Eval课程中使用该方法。

3 个答案:

答案 0 :(得分:3)

我最终使用了以下活动。它无法访问工作流的变量,而是接受单个参数“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

答案 1 :(得分:0)

我建议为此使用不同的框架。一个好方法是使用nCalc。 http://ncalc.codeplex.com/

它可以解析任何表达式并评估结果,包括静态或动态参数和自定义函数 我们用它来评估运行时不同类型的表达式。

答案 2 :(得分:0)

如果你的'谓词'是一个众所周知的字符串,并且不需要是在运行时评估的表达式,你肯定可以做这样的事情,抛弃 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()方法更改子实现时没有任何效果。执行模式已启用且子树无法更改。