| | | 1 | | using System.ComponentModel; |
| | | 2 | | using System.ComponentModel.DataAnnotations; |
| | | 3 | | using System.Reflection; |
| | | 4 | | using Elsa.Extensions; |
| | | 5 | | using Elsa.Workflows.Attributes; |
| | | 6 | | using Elsa.Workflows.Management.Activities.HostMethod; |
| | | 7 | | using Elsa.Workflows.Management.Attributes; |
| | | 8 | | using Elsa.Workflows.Models; |
| | | 9 | | using Humanizer; |
| | | 10 | | |
| | | 11 | | namespace Elsa.Workflows.Management.Services; |
| | | 12 | | |
| | 441 | 13 | | public class HostMethodActivityDescriber(IActivityDescriber activityDescriber) : IHostMethodActivityDescriber |
| | | 14 | | { |
| | | 15 | | public async Task<IEnumerable<ActivityDescriptor>> DescribeAsync(string key, Type hostType, CancellationToken cancel |
| | | 16 | | { |
| | 28 | 17 | | var methods = hostType |
| | 28 | 18 | | .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly) |
| | 252 | 19 | | .Where(m => !m.IsSpecialName) |
| | 28 | 20 | | .ToList(); |
| | | 21 | | |
| | 28 | 22 | | var descriptors = new List<ActivityDescriptor>(methods.Count); |
| | 560 | 23 | | foreach (var method in methods) |
| | | 24 | | { |
| | 252 | 25 | | var descriptor = await DescribeMethodAsync(key, hostType, method, cancellationToken); |
| | 252 | 26 | | descriptors.Add(descriptor); |
| | | 27 | | } |
| | | 28 | | |
| | 28 | 29 | | return descriptors; |
| | 28 | 30 | | } |
| | | 31 | | |
| | | 32 | | public async Task<ActivityDescriptor> DescribeMethodAsync(string key, Type hostType, MethodInfo method, Cancellation |
| | | 33 | | { |
| | 252 | 34 | | var descriptor = await activityDescriber.DescribeActivityAsync(typeof(HostMethodActivity), cancellationToken); |
| | 252 | 35 | | var activityAttribute = hostType.GetCustomAttribute<ActivityAttribute>() ?? method.GetCustomAttribute<ActivityAt |
| | | 36 | | |
| | 252 | 37 | | var methodName = method.Name; |
| | 252 | 38 | | var activityTypeName = BuildActivityTypeName(key, method, activityAttribute); |
| | | 39 | | |
| | 252 | 40 | | var displayAttribute = method.GetCustomAttribute<DisplayAttribute>(); |
| | 252 | 41 | | var typeDisplayName = activityAttribute?.DisplayName ?? hostType.GetCustomAttribute<DisplayNameAttribute>()?.Dis |
| | 252 | 42 | | var methodNameWithoutAsync = StripAsyncSuffix(methodName); |
| | 252 | 43 | | var methodDisplayName = displayAttribute?.Name ?? methodNameWithoutAsync.Humanize().Transform(To.TitleCase); |
| | 252 | 44 | | var displayName = !string.IsNullOrWhiteSpace(typeDisplayName) ? typeDisplayName : methodDisplayName; |
| | 252 | 45 | | if (!string.IsNullOrWhiteSpace(activityAttribute?.DisplayName)) |
| | 14 | 46 | | displayName = activityAttribute.DisplayName!; |
| | | 47 | | |
| | 252 | 48 | | descriptor.Name = methodName; |
| | 252 | 49 | | descriptor.TypeName = activityTypeName; |
| | 252 | 50 | | descriptor.DisplayName = displayName; |
| | 252 | 51 | | descriptor.Description = activityAttribute?.Description ?? method.GetCustomAttribute<DescriptionAttribute>()?.De |
| | 252 | 52 | | descriptor.Category = activityAttribute?.Category ?? hostType.Name.Humanize().Transform(To.TitleCase); |
| | 252 | 53 | | descriptor.Kind = activityAttribute?.Kind ?? ActivityKind.Task; |
| | 252 | 54 | | descriptor.RunAsynchronously = activityAttribute?.RunAsynchronously ?? false; |
| | 252 | 55 | | descriptor.IsBrowsable = true; |
| | 252 | 56 | | descriptor.ClrType = typeof(HostMethodActivity); |
| | | 57 | | |
| | 252 | 58 | | descriptor.Constructor = context => |
| | 252 | 59 | | { |
| | 0 | 60 | | var activity = context.CreateActivity<HostMethodActivity>(); |
| | 0 | 61 | | activity.Type = activityTypeName; |
| | 0 | 62 | | activity.HostType = hostType; |
| | 0 | 63 | | activity.MethodName = methodName; |
| | 0 | 64 | | activity.RunAsynchronously ??= descriptor.RunAsynchronously; |
| | 0 | 65 | | return activity; |
| | 252 | 66 | | }; |
| | | 67 | | |
| | 252 | 68 | | descriptor.Inputs.Clear(); |
| | 504 | 69 | | foreach (var prop in hostType.GetProperties(BindingFlags.Instance | BindingFlags.Public)) |
| | | 70 | | { |
| | 0 | 71 | | if (!IsInputProperty(prop)) |
| | | 72 | | continue; |
| | | 73 | | |
| | 0 | 74 | | var inputDescriptor = CreatePropertyInputDescriptor(prop); |
| | 0 | 75 | | descriptor.Inputs.Add(inputDescriptor); |
| | | 76 | | } |
| | | 77 | | |
| | 1036 | 78 | | foreach (var parameter in method.GetParameters()) |
| | | 79 | | { |
| | 266 | 80 | | if (IsSpecialParameter(parameter)) |
| | | 81 | | continue; |
| | | 82 | | |
| | | 83 | | // If FromServices is used, the parameter is not a workflow input unless explicitly forced via [Input]. |
| | 210 | 84 | | var isFromServices = parameter.GetCustomAttribute<FromServicesAttribute>() != null; |
| | 210 | 85 | | var isExplicitInput = parameter.GetCustomAttribute<InputAttribute>() != null; |
| | 210 | 86 | | if (isFromServices && !isExplicitInput) |
| | | 87 | | continue; |
| | | 88 | | |
| | 210 | 89 | | var inputDescriptor = CreateParameterInputDescriptor(parameter); |
| | 210 | 90 | | descriptor.Inputs.Add(inputDescriptor); |
| | | 91 | | } |
| | | 92 | | |
| | 252 | 93 | | descriptor.Outputs.Clear(); |
| | 252 | 94 | | var outputDescriptor = CreateOutputDescriptor(method); |
| | 252 | 95 | | if (outputDescriptor != null) |
| | 84 | 96 | | descriptor.Outputs.Add(outputDescriptor); |
| | | 97 | | |
| | 252 | 98 | | return descriptor; |
| | 252 | 99 | | } |
| | | 100 | | |
| | | 101 | | private string BuildActivityTypeName(string key, MethodInfo method, ActivityAttribute? activityAttribute) |
| | | 102 | | { |
| | 252 | 103 | | var methodName = StripAsyncSuffix(method.Name); |
| | | 104 | | |
| | 252 | 105 | | if (activityAttribute != null && !string.IsNullOrWhiteSpace(activityAttribute.Namespace)) |
| | | 106 | | { |
| | 14 | 107 | | var typeSegment = activityAttribute.Type ?? methodName; |
| | 14 | 108 | | return $"{activityAttribute.Namespace}.{typeSegment}"; |
| | | 109 | | } |
| | | 110 | | |
| | 238 | 111 | | return $"Elsa.Dynamic.HostMethod.{key.Pascalize()}.{methodName}"; |
| | | 112 | | } |
| | | 113 | | |
| | | 114 | | private static string StripAsyncSuffix(string name) |
| | | 115 | | { |
| | 504 | 116 | | return name.EndsWith("Async", StringComparison.Ordinal) |
| | 504 | 117 | | ? name[..^5] |
| | 504 | 118 | | : name; |
| | | 119 | | } |
| | | 120 | | |
| | | 121 | | private InputDescriptor CreatePropertyInputDescriptor(PropertyInfo prop) |
| | | 122 | | { |
| | 0 | 123 | | var inputAttribute = prop.GetCustomAttribute<InputAttribute>(); |
| | 0 | 124 | | var displayNameAttribute = prop.GetCustomAttribute<DisplayNameAttribute>(); |
| | 0 | 125 | | var descriptionAttribute = prop.GetCustomAttribute<DescriptionAttribute>(); |
| | | 126 | | |
| | 0 | 127 | | var inputName = inputAttribute?.Name ?? prop.Name; |
| | 0 | 128 | | var displayName = inputAttribute?.DisplayName ?? displayNameAttribute?.DisplayName ?? prop.Name.Humanize(); |
| | 0 | 129 | | var description = inputAttribute?.Description ?? descriptionAttribute?.Description; |
| | 0 | 130 | | var nakedInputType = prop.PropertyType; |
| | | 131 | | |
| | 0 | 132 | | return new() |
| | 0 | 133 | | { |
| | 0 | 134 | | Name = inputName, |
| | 0 | 135 | | DisplayName = displayName, |
| | 0 | 136 | | Description = description, |
| | 0 | 137 | | Type = nakedInputType, |
| | 0 | 138 | | ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(inputName), |
| | 0 | 139 | | ValueSetter = (activity, value) => activity.SyntheticProperties[inputName] = value!, |
| | 0 | 140 | | IsSynthetic = true, |
| | 0 | 141 | | IsWrapped = true, |
| | 0 | 142 | | UIHint = inputAttribute?.UIHint ?? ActivityDescriber.GetUIHint(nakedInputType), |
| | 0 | 143 | | Category = inputAttribute?.Category, |
| | 0 | 144 | | DefaultValue = inputAttribute?.DefaultValue, |
| | 0 | 145 | | Order = inputAttribute?.Order ?? 0, |
| | 0 | 146 | | IsBrowsable = inputAttribute?.IsBrowsable ?? true, |
| | 0 | 147 | | AutoEvaluate = inputAttribute?.AutoEvaluate ?? true, |
| | 0 | 148 | | IsSerializable = inputAttribute?.IsSerializable ?? true |
| | 0 | 149 | | }; |
| | | 150 | | } |
| | | 151 | | |
| | | 152 | | private InputDescriptor CreateParameterInputDescriptor(ParameterInfo parameter) |
| | | 153 | | { |
| | 210 | 154 | | var inputAttribute = parameter.GetCustomAttribute<InputAttribute>(); |
| | 210 | 155 | | var displayNameAttribute = parameter.GetCustomAttribute<DisplayNameAttribute>(); |
| | | 156 | | |
| | 210 | 157 | | var inputName = inputAttribute?.Name ?? parameter.Name ?? "input"; |
| | 210 | 158 | | var displayName = inputAttribute?.DisplayName ?? displayNameAttribute?.DisplayName ?? inputName.Humanize(); |
| | 210 | 159 | | var description = inputAttribute?.Description; |
| | 210 | 160 | | var nakedInputType = parameter.ParameterType; |
| | | 161 | | |
| | 210 | 162 | | return new() |
| | 210 | 163 | | { |
| | 210 | 164 | | Name = inputName, |
| | 210 | 165 | | DisplayName = displayName, |
| | 210 | 166 | | Description = description, |
| | 210 | 167 | | Type = nakedInputType, |
| | 0 | 168 | | ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(inputName), |
| | 0 | 169 | | ValueSetter = (activity, value) => activity.SyntheticProperties[inputName] = value!, |
| | 210 | 170 | | IsSynthetic = true, |
| | 210 | 171 | | IsWrapped = true, |
| | 210 | 172 | | UIHint = inputAttribute?.UIHint ?? ActivityDescriber.GetUIHint(nakedInputType), |
| | 210 | 173 | | Category = inputAttribute?.Category, |
| | 210 | 174 | | DefaultValue = inputAttribute?.DefaultValue, |
| | 210 | 175 | | Order = inputAttribute?.Order ?? 0, |
| | 210 | 176 | | IsBrowsable = inputAttribute?.IsBrowsable ?? true, |
| | 210 | 177 | | AutoEvaluate = inputAttribute?.AutoEvaluate ?? true, |
| | 210 | 178 | | IsSerializable = inputAttribute?.IsSerializable ?? true |
| | 210 | 179 | | }; |
| | | 180 | | } |
| | | 181 | | |
| | | 182 | | private OutputDescriptor? CreateOutputDescriptor(MethodInfo method) |
| | | 183 | | { |
| | 252 | 184 | | var returnType = method.ReturnType; |
| | | 185 | | |
| | | 186 | | // No output for void or Task. |
| | 252 | 187 | | if (returnType == typeof(void) || returnType == typeof(Task)) |
| | 168 | 188 | | return null; |
| | | 189 | | |
| | | 190 | | // Determine the "real" return type. |
| | | 191 | | Type actualReturnType; |
| | 84 | 192 | | if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>)) |
| | 14 | 193 | | actualReturnType = returnType.GetGenericArguments()[0]; |
| | 70 | 194 | | else if (typeof(Task).IsAssignableFrom(returnType)) |
| | 0 | 195 | | return null; |
| | | 196 | | else |
| | 70 | 197 | | actualReturnType = returnType; |
| | | 198 | | |
| | 84 | 199 | | var outputAttribute = method.ReturnParameter.GetCustomAttribute<OutputAttribute>() ?? |
| | 84 | 200 | | method.GetCustomAttribute<OutputAttribute>() ?? |
| | 84 | 201 | | method.DeclaringType?.GetCustomAttribute<OutputAttribute>(); |
| | | 202 | | |
| | 84 | 203 | | var displayNameAttribute = method.ReturnParameter.GetCustomAttribute<DisplayNameAttribute>(); |
| | 84 | 204 | | var outputName = outputAttribute?.Name ?? "Output"; |
| | 84 | 205 | | var displayName = outputAttribute?.DisplayName ?? displayNameAttribute?.DisplayName ?? outputName.Humanize(); |
| | 84 | 206 | | var description = outputAttribute?.Description ?? "The method output."; |
| | 84 | 207 | | var nakedOutputType = actualReturnType; |
| | | 208 | | |
| | 84 | 209 | | return new() |
| | 84 | 210 | | { |
| | 84 | 211 | | Name = outputName, |
| | 84 | 212 | | DisplayName = displayName, |
| | 84 | 213 | | Description = description, |
| | 84 | 214 | | Type = nakedOutputType, |
| | 84 | 215 | | IsSynthetic = true, |
| | 0 | 216 | | ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(outputName), |
| | 0 | 217 | | ValueSetter = (activity, value) => activity.SyntheticProperties[outputName] = value!, |
| | 84 | 218 | | IsBrowsable = outputAttribute?.IsBrowsable ?? true, |
| | 84 | 219 | | IsSerializable = outputAttribute?.IsSerializable ?? true |
| | 84 | 220 | | }; |
| | | 221 | | } |
| | | 222 | | |
| | | 223 | | private static bool IsSpecialParameter(ParameterInfo parameter) |
| | | 224 | | { |
| | | 225 | | // These parameters are supplied by the runtime and should not become input descriptors. |
| | 266 | 226 | | if (parameter.ParameterType == typeof(CancellationToken)) |
| | 14 | 227 | | return true; |
| | | 228 | | |
| | 252 | 229 | | if (parameter.ParameterType == typeof(ActivityExecutionContext)) |
| | 42 | 230 | | return true; |
| | | 231 | | |
| | 210 | 232 | | return false; |
| | | 233 | | } |
| | | 234 | | |
| | | 235 | | private static bool IsInputProperty(PropertyInfo prop) |
| | | 236 | | { |
| | 0 | 237 | | if (!prop.CanRead || !prop.CanWrite) |
| | 0 | 238 | | return false; |
| | | 239 | | |
| | 0 | 240 | | if (prop.GetIndexParameters().Length > 0) |
| | 0 | 241 | | return false; |
| | | 242 | | |
| | 0 | 243 | | return true; |
| | | 244 | | } |
| | | 245 | | } |