问题
I am trying to create a simple WF4 activity that accepts a string that contains a VB.NET expression (from say the database), evaluates that string using the variables available in the current scope of the workflow and returns the result. Unfortunately, with the ways I've tried it, whether it be with a plain on Activity
or a full-fledged NativeActivity
, I keep hitting a wall.
My first attempt was with a simple Activity, and I was able to make a simple class that evaluates an expression given some object as its input:
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 } });
}
}
This woks nicely, and the following expression evaluates to 7:
new Eval<int, int>("Value + 2").EvalWith(5)
Unfortunately, I can't use it the way I want since the expression string is given as a constructor argument instead of as an InArgument<string>
, so it can't be easily incorporated (dragged and dropped) into a workflow. My second attempt was to try and use NativeActivity
to get rid of that pesky constructor parameter:
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));
}
}
I tried running NativeEval
with the following:
WorkflowInvoker.Invoke(new NativeEval<int, int>(), new Dictionary<string, object>
{ { "ExpressionText", "Value + 2" }, { "Value", 5 } });
But got the following exception:
Activity '1: NativeEval' cannot access this variable because it is declared at the scope of activity '1: NativeEval'. An activity can only access its own implementation variables.
So I changed metadata.AddVariable(ResultVar);
to metadata.AddImplementationVariable(ResultVar);
but then I got a different exception:
The following errors were encountered while processing the workflow tree: 'VariableReference': The referenced Variable object (Name = 'ResultVar') is not visible at this scope. There may be another location reference with the same name that is visible at this scope, but it does not reference the same location.
I tried using .ScheduleFunc()
as described here to schedule a VisualBasicValue
activity, but the result it returned was always null
(but oddly enough no exceptions were thrown).
I'm stumped. The metaprogramming model of WF4 seems much more difficult than the metaprogramming model of System.Linq.Expressions
, which albeit difficult and often perplexing (like metaprogramming usually is), at least I was able to wrap my head around it. I guess it's because it has the added complexity of needing to represent a persistable, resumable, asynchronous, relocatable program, rather than just a plain old program.
EDIT: Since I don't think the issue I'm experiencing is caused by the fact that I'm trying to evaluate an expression that isn't hardcoded, the following alteration can be made to the NativeActivity
that cause it to have a static expression:
Replace
Predicate = new VisualBasicValue<TResult>();
With
Predicate = new VisualBasicValue<TResult>("ExpressionText.Length");
And remove the line
Predicate.ExpressionText = ExpressionText.Get(context);
Now even though with those lines the expression is static, I'm still getting the same errors.
EDIT2: This article addressed the exception I was getting. I had to change both variable and child activity to be an "implementation", so this:
metadata.AddVariable(ResultVar);
metadata.AddChild(Assign);
Changed to this:
metadata.AddImplementationVariable(ResultVar);
metadata.AddImplementationChild(Assign);
And caused all the exceptions to go away. Unfortunately, it revealed that the following line does absolutely nothing:
Predicate.ExpressionText = ExpressionText.Get(context);
Changing the ExpressionText
property of a VisualBasicValue
during runtime has no effect. A quick check with ILSpy reveals why - the expression text is only evaluated and converted to an expression tree when CacheMetadata()
is called, at which point the expression is not yet know, which is why I used the parameterless constructor which initialized and crystallized the expression to a no-op. I even tried saving the NativeActivityMetadata
object I got in my own CacheMetadata overridden method and then use reflection to force a call to VisualBasicValue
's CacheMetadata()
, but that just ended up throwing a different cryptic exception ("Ambiguous match found." of type AmbiguousMatchException).
At this point it doesn't seem possible to fully integrate a dynamic expression into a workflow, exposing all the in-scope variables to it. I guess I'll have the method used in my Eval
class within the NativeEval
class.
回答1:
I ended up using the following activity. It can't access the workflow's variables, instead it accepts a single argument 'Value' that can be used by the same name inside the dynamic expression. Other than that it works pretty well.
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);
}
}
}
EDIT: Updated the class to include complete assembly and namespace imports, lest you get the dreaded (and unhelpful) error message:
'Value' is not declared. It may be inaccessible due to its protection level.
Also, moved the ExpressionEvaluator class outside and made it public, so you can used it outside of WF, like so:
new ExpressionEvaluator<int, double>("Value * Math.PI").EvalWith(2);
Which will return:
6.28318530717959
回答2:
I would suggest to use a different framework for this. One good approach is to use nCalc. http://ncalc.codeplex.com/
It can parse any expression and evaluate the result, including static or dynamic parameters and custom functions.
We use it to evaluate different kind of expressions at runtime.
回答3:
If your 'predicate' is a well-known string and don't need to be an expression evaluated at runtime you surely can do something like this, throwing away the InArgument and avoid the constructor:
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();
}
}
}
and call it this way:
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));
EDIT: check that even with Predicate.ExpressionText
not commented, it has no effect whatsoever:
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));
}
}
When you get at Execute()
method changing the child implementation has no effect. The execution mode is on and the children tree cannot be altered.
来源:https://stackoverflow.com/questions/10284703/wf4-how-do-i-evaluate-an-expression-only-known-at-runtime