< Summary

Information
Class: Elsa.Workflows.Management.Activities.HostMethod.HostMethodActivity
Assembly: Elsa.Workflows.Management
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Management/Activities/HostMethod/HostMethodActivity.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 92
Coverable lines: 92
Total lines: 193
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 48
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_HostType()100%210%
get_MethodName()100%210%
ExecuteAsync()100%210%
ResumeAsync()0%2040%
ExecuteInternalAsync()0%7280%
ResolveMethod(...)0%620%
BuildArgumentsAsync()0%156120%
ApplyPropertyInputs(...)0%7280%
InvokeAndGetResultAsync()0%110100%
SetOutput(...)0%620%
ConvertIfNeeded(...)0%620%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Management/Activities/HostMethod/HostMethodActivity.cs

#LineLine coverage
 1using System.ComponentModel;
 2using System.Dynamic;
 3using System.Reflection;
 4using System.Text.Json.Serialization;
 5using Elsa.Expressions.Helpers;
 6using Elsa.Workflows.Management.Services;
 7using Elsa.Workflows.Models;
 8using Microsoft.Extensions.DependencyInjection;
 9
 10namespace Elsa.Workflows.Management.Activities.HostMethod;
 11
 12/// <summary>
 13/// Executes a public async method on a configured CLR type. Internal activity used by <see cref="HostMethodActivityProv
 14/// </summary>
 15[Browsable(false)]
 16public class HostMethodActivity : Activity
 17{
 018    [JsonIgnore] internal Type HostType { get; set; } = null!;
 019    [JsonIgnore] internal string MethodName { get; set; } = null!;
 20
 21    /// <inheritdoc />
 22    protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
 23    {
 024        var method = ResolveMethod(MethodName);
 025        await ExecuteInternalAsync(context, method);
 026    }
 27
 28    private async ValueTask ResumeAsync(ActivityExecutionContext context)
 29    {
 030        var metadata = context.WorkflowExecutionContext.ResumedBookmarkContext?.Bookmark.Metadata;
 031        if (metadata != null)
 32        {
 033            var callbackMethodName = metadata["HostMethodActivityResumeCallback"];
 034            var callbackMethod = ResolveMethod(callbackMethodName);
 035            await ExecuteInternalAsync(context, callbackMethod);
 36        }
 037    }
 38
 39    private async Task ExecuteInternalAsync(ActivityExecutionContext context, MethodInfo method)
 40    {
 041        var cancellationToken = context.CancellationToken;
 042        var activityDescriptor = context.ActivityDescriptor;
 043        var inputDescriptors = activityDescriptor.Inputs.ToList();
 044        var serviceProvider = context.GetRequiredService<IServiceProvider>();
 045        var hostInstance = ActivatorUtilities.CreateInstance(serviceProvider, HostType);
 046        var args = await BuildArgumentsAsync(method, inputDescriptors, context, serviceProvider, cancellationToken);
 047        var currentBookmarks = context.Bookmarks.ToList();
 48
 049        ApplyPropertyInputs(hostInstance, inputDescriptors, context);
 50
 051        var resultValue = await InvokeAndGetResultAsync(hostInstance, method, args);
 052        SetOutput(activityDescriptor, resultValue, context);
 53
 54        // By convention, if no bookmarks are created, complete the activity. This may change in the future when we expo
 055        var addedBookmarks = context.Bookmarks.Except(currentBookmarks).ToList();
 056        if (!addedBookmarks.Any())
 57        {
 058            await context.CompleteActivityAsync();
 059            return;
 60        }
 61
 62        // If bookmarks were created, overwrite the resume callback. We need to invoke the callback provided by the host
 063        foreach (var bookmark in addedBookmarks)
 64        {
 065            var callbackMethodName = bookmark.CallbackMethodName;
 66
 067            if (string.IsNullOrWhiteSpace(callbackMethodName))
 68                continue;
 69
 070            bookmark.CallbackMethodName = nameof(ResumeAsync);
 071            var metadata = bookmark.Metadata ?? new Dictionary<string, string>();
 72
 073            metadata["HostMethodActivityResumeCallback"] = callbackMethodName;
 074            bookmark.Metadata = metadata;
 75        }
 076    }
 77
 78    private MethodInfo ResolveMethod(string methodName)
 79    {
 080        var method = HostType.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | 
 081        if (method == null)
 082            throw new InvalidOperationException($"Method '{methodName}' not found on type '{HostType.Name}'.");
 83
 084        return method;
 85    }
 86
 87    private async ValueTask<object?[]> BuildArgumentsAsync(MethodInfo method, IReadOnlyCollection<InputDescriptor> input
 88    {
 089        var parameters = method.GetParameters();
 090        var args = new object?[parameters.Length];
 91
 92        // Allow multiple providers; call in order.
 093        var providers = serviceProvider.GetServices<IHostMethodParameterValueProvider>().ToList();
 094        if (providers.Count == 0)
 095            providers.Add(new DefaultHostMethodParameterValueProvider());
 96
 097        for (var i = 0; i < parameters.Length; i++)
 98        {
 099            var parameter = parameters[i];
 100
 0101            var providerContext = new HostMethodParameterValueProviderContext(
 0102                serviceProvider,
 0103                context,
 0104                inputDescriptors,
 0105                this,
 0106                parameter,
 0107                cancellationToken);
 108
 0109            var handled = false;
 0110            object? value = null;
 111
 0112            foreach (var provider in providers)
 113            {
 0114                var result = await provider.GetValueAsync(providerContext);
 0115                if (!result.Handled)
 116                    continue;
 117
 0118                handled = true;
 0119                value = result.Value;
 0120                break;
 121            }
 122
 0123            if (handled)
 124            {
 0125                args[i] = value;
 0126                continue;
 127            }
 128
 129            // No provider handled it: fall back to parameter default value (if any).
 0130            args[i] = parameter.HasDefaultValue ? parameter.DefaultValue : null;
 0131        }
 132
 0133        return args;
 0134    }
 135
 136    private void ApplyPropertyInputs(object hostInstance, IReadOnlyCollection<InputDescriptor> inputDescriptors, Activit
 137    {
 0138        var hostPropertyLookup = HostType.GetProperties().ToDictionary(x => x.Name, x => x);
 139
 0140        foreach (var inputDescriptor in inputDescriptors)
 141        {
 0142            if (!hostPropertyLookup.TryGetValue(inputDescriptor.Name, out var prop) || !prop.CanWrite)
 143                continue;
 144
 0145            var input = (Input?)inputDescriptor.ValueGetter(this);
 0146            var inputValue = input != null ? context.Get(input.MemoryBlockReference()) : null;
 0147            inputValue = ConvertIfNeeded(inputValue, prop.PropertyType);
 148
 0149            prop.SetValue(hostInstance, inputValue);
 150        }
 0151    }
 152
 153    private async Task<object?> InvokeAndGetResultAsync(object hostInstance, MethodInfo method, object?[] args)
 154    {
 0155        var invocationResult = method.Invoke(hostInstance, args);
 156
 157        // Synchronous methods.
 0158        if (invocationResult is not Task task)
 159        {
 0160            return method.ReturnType == typeof(void) ? null : invocationResult;
 161        }
 162
 0163        await task;
 164
 165        // Task<T>.
 0166        if (method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))
 167        {
 0168            var resultProperty = task.GetType().GetProperty("Result");
 0169            return resultProperty?.GetValue(task);
 170        }
 171
 172        // Task.
 0173        return null;
 0174    }
 175
 176    private void SetOutput(ActivityDescriptor activityDescriptor, object? resultValue, ActivityExecutionContext context)
 177    {
 0178        var outputDescriptor = activityDescriptor.Outputs.SingleOrDefault();
 0179        if (outputDescriptor == null)
 0180            return;
 181
 0182        var output = (Output?)outputDescriptor.ValueGetter(this);
 0183        context.Set(output, resultValue, outputDescriptor.Name);
 0184    }
 185
 186    private static object? ConvertIfNeeded(object? value, Type targetType)
 187    {
 0188        if (value is ExpandoObject expandoObject)
 0189            return expandoObject.ConvertTo(targetType);
 190
 0191        return value;
 192    }
 193}