Skip to content

Expression-based activity invocations #1498

@Arithmomaniac

Description

@Arithmomaniac

I'd like to be able to use expressions to call context.CallActivityAsync. This would provide stronger typing and remove the "magic string" for the function name.

e.g. given the following function

[FunctionName(nameof(GetMyObject))]
public async Task<MyObject> GetMyObject([ActivityTrigger] string input) { /*...*/ }

You could use
context.CallActivityAsync(() => GetMyObject("Hello"))
instead of the current
context.CallActivityAsync<MyObject>(nameof(GetMyObject), "Hello")

The expression syntax could call the existing version for its implementation. A naive implementation would be something like:

Task<TResult> CallActivityAsync<TResult>(Expression<Func<TResult>> expr)
{
    var body = (MethodCallExpression)expr.Body;
    var functionName = body.Method.GetCustomAttributesData()
        .Single(x => x.AttributeType == typeof(FunctionNameAttribute))
        .ConstructorArguments[0].Value;
    var triggerArgExpr = body.Method.GetParameters().Zip(body.Arguments, ValueTuple.Create)
        .First(x => x.Item1.GetCustomAttribute(typeof(ActivityTriggerAttribute)) != null)
        .Item2;
    var arg = Expression.Lambda(triggerArgExpr).Compile().DynamicInvoke();
    return CallActivityAsync<TResult>(functionName, arg);
}

Some further thoughts:

  • There are more efficient ways of resolving the value than DynamicInvoke; see e.g. here.
  • If there are further parameters (e.g., injected ILog) the values would be ignored.
  • A Roslyn Analyzer could check for the following:
    • The method called is an activity with a function name (required / error level)
    • The expression is a MethodCallExpression (required / error level)
    • All non-ActivityTrigger parameters are their default values (optional / warning level)
  • This works for static activity functions and activity functions in the current class. In a different class, you'd need something with a signature like Task<TResult> CallActivityAsync<TClass, TResult>(Expression<Func<TClass, TResult>> expr) , which would then be called like context.CallActivityAsync<DiffClass, MyObject>(c => c.GetMyObject("Hello")).
    • Note how the expression parameter would not be allowed to reference c.

Of course, you could have Task-returning and CallActivityWithRetryAsync overloads as well.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions