Skip to content

Commit

Permalink
Define inputs in reusable objects (elsa-workflows#3148)
Browse files Browse the repository at this point in the history
* Parse object to get inputs

* properties name
  • Loading branch information
stuartmcgillivray authored Jun 23, 2022
1 parent da901d4 commit c4bade7
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;

namespace Elsa.Attributes
{
[AttributeUsage(AttributeTargets.Property)]
public class ActivityInputObjectAttribute : Attribute
{
/// <summary>
/// A category to group these property with - will be overidden by any inputs that specify their own category.
/// </summary>
public string? Category { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ public void AddProvider(string activityId, string propertyName, IActivityPropert
properties[propertyName] = provider;
}

public IDictionary<string, IActivityPropertyValueProvider> GetProviders(string activityId) =>
_providers.TryGetValue(activityId, out var properties)
? properties ?? new Dictionary<string, IActivityPropertyValueProvider>()
public IDictionary<string, IActivityPropertyValueProvider> GetProviders(string activityId) =>
_providers.TryGetValue(activityId, out var properties)
? properties ?? new Dictionary<string, IActivityPropertyValueProvider>()
: new Dictionary<string, IActivityPropertyValueProvider>();

public IActivityPropertyValueProvider? GetProvider(string activityId, string propertyName) =>
Expand All @@ -51,34 +51,52 @@ public IDictionary<string, IActivityPropertyValueProvider> GetProviders(string a

public async ValueTask SetActivityPropertiesAsync(IActivity activity, ActivityExecutionContext activityExecutionContext, CancellationToken cancellationToken = default)
{
var properties = activity.GetType().GetProperties().Where(IsActivityInputProperty).ToList();
var providers = GetProviders(activity.Id);
var defaultValueResolver = activityExecutionContext.GetService<IActivityPropertyDefaultValueResolver>();

await SetNestedActivityPropertiesAsync(activity, activityExecutionContext, providers, defaultValueResolver, null, cancellationToken);
}

private async ValueTask SetNestedActivityPropertiesAsync(object nestedInstance, ActivityExecutionContext activityExecutionContext, IDictionary<string, IActivityPropertyValueProvider> providers, IActivityPropertyDefaultValueResolver defaultValueResolver, string nestedInstanceName = null, CancellationToken cancellationToken = default)
{
var properties = nestedInstance.GetType().GetProperties().Where(IsActivityInputProperty).ToList();
var nestedProperties = nestedInstance.GetType().GetProperties().Where(IsActivityObjectInputProperty).ToList();

foreach (var property in properties)
{
if (!providers.TryGetValue(property.Name, out var provider))
var propertyName = nestedInstanceName == null ? property.Name : $"{nestedInstanceName}_{property.Name}";
if (!providers.TryGetValue(propertyName, out var provider))
continue;

try
{
var value = await provider.GetValueAsync(activityExecutionContext, cancellationToken);

if (value == null)
{
value = defaultValueResolver.GetDefaultValue(property);
}

if (value != null) property.SetValue(activity, value);
if (value != null) property.SetValue(nestedInstance, value);
}
catch (Exception e)
{
throw new CannotSetActivityPropertyValueException($@"An exception was thrown whilst setting '{activity?.GetType().Name}.{property.Name}'. See the inner exception for further details.", e);
throw new CannotSetActivityPropertyValueException($@"An exception was thrown whilst setting '{nestedInstance.GetType().Name}.{property.Name}'. See the inner exception for further details.", e);
}
}
}

foreach (var nestedProperty in nestedProperties)
{
var instance = Activator.CreateInstance(nestedProperty.PropertyType);

var nextInstanceName = nestedInstanceName == null ? nestedProperty.Name : $"{nestedInstanceName}_{nestedProperty.Name}";

await SetNestedActivityPropertiesAsync(instance, activityExecutionContext, providers, defaultValueResolver, nextInstanceName, cancellationToken);
nestedProperty.SetValue(nestedInstance, instance);
}
}
private bool IsActivityInputProperty(PropertyInfo property) => property.GetCustomAttribute<ActivityInputAttribute>() != null;
private bool IsActivityObjectInputProperty(PropertyInfo property) => property.GetCustomAttribute<ActivityInputObjectAttribute>() != null;
public IEnumerator<KeyValuePair<string, IDictionary<string, IActivityPropertyValueProvider>>> GetEnumerator() => _providers.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Expand Down
21 changes: 19 additions & 2 deletions src/core/Elsa.Core/Metadata/TypedActivityTypeDescriber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,27 @@ private async Task<string[]> GetOutcomesAsync(ActivityAttribute? activityAttribu
throw new NotSupportedException("The specified outcomes type is not supported. Only string[] and typeof(IOutcomesProvider) are supported.");
}

private IEnumerable<ActivityInputDescriptor> DescribeInputProperties(IEnumerable<PropertyInfo> properties)
private IEnumerable<ActivityInputDescriptor> DescribeInputProperties(IEnumerable<PropertyInfo> properties, string category = null)
{
foreach (var propertyInfo in properties)
{
var activityPropertyObjectAttribute = propertyInfo.GetCustomAttribute<ActivityInputObjectAttribute>();

if(activityPropertyObjectAttribute != null)
{

var objectProperties = propertyInfo.PropertyType.GetProperties();

var nestedInputProperties = DescribeInputProperties(objectProperties, activityPropertyObjectAttribute?.Category);

foreach (var property in nestedInputProperties)
{
property.Name = $"{propertyInfo.Name}_{property.Name}";
yield return property;
}
continue;
}

var activityPropertyAttribute = propertyInfo.GetCustomAttribute<ActivityInputAttribute>();

if (activityPropertyAttribute == null)
Expand All @@ -91,7 +108,7 @@ private IEnumerable<ActivityInputDescriptor> DescribeInputProperties(IEnumerable
activityPropertyAttribute.Label ?? propertyInfo.Name.Humanize(LetterCasing.Title),
activityPropertyAttribute.Hint,
_optionsResolver.GetOptions(propertyInfo),
activityPropertyAttribute.Category,
activityPropertyAttribute.Category ?? category,
activityPropertyAttribute.Order,
_defaultValueResolver.GetDefaultValue(propertyInfo),
activityPropertyAttribute.DefaultSyntax,
Expand Down
61 changes: 56 additions & 5 deletions src/core/Elsa.Core/Services/Workflows/ActivityActivator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
Expand Down Expand Up @@ -46,37 +47,65 @@ public async Task<IActivity> ActivateActivityAsync(ActivityExecutionContext cont
}

private async ValueTask ApplyStoredValuesAsync(ActivityExecutionContext context, IActivity activity, CancellationToken cancellationToken)
{
await ApplyStoredObjectValuesAsync(context, activity, cancellationToken);
}

private async ValueTask ApplyStoredObjectValuesAsync(ActivityExecutionContext context, object activity, CancellationToken cancellationToken, string parentName = null)
{
var properties = activity.GetType().GetProperties().Where(IsActivityProperty).ToList();
var nestedProperties = activity.GetType().GetProperties().Where(IsActivityObjectProperty).ToList();
var propertyStorageProviderDictionary = context.ActivityBlueprint.PropertyStorageProviders;
var workflowStorageContext = new WorkflowStorageContext(context.WorkflowInstance, context.ActivityId);

foreach (var property in properties)
{
var propertyName = parentName == null ? property.Name : $"{parentName}_{property.Name}";
var attr = property.GetCustomAttribute<ActivityPropertyAttributeBase>();
var providerName = propertyStorageProviderDictionary.GetItem(property.Name) ?? attr.DefaultWorkflowStorageProvider;
var value = await _workflowStorageService.LoadAsync( providerName, workflowStorageContext, property.Name, cancellationToken);
var providerName = propertyStorageProviderDictionary.GetItem(propertyName) ?? attr.DefaultWorkflowStorageProvider;
var value = await _workflowStorageService.LoadAsync(providerName, workflowStorageContext, propertyName, cancellationToken);

if (value != null)
{
var typedValue = value.ConvertTo(property.PropertyType);
property.SetValue(activity, typedValue);
}
}

foreach (var nestedProperty in nestedProperties)
{
var instance = Activator.CreateInstance(nestedProperty.PropertyType);
var propertyName = parentName == null ? nestedProperty.Name : $"{parentName}_{nestedProperty.Name}";
await ApplyStoredObjectValuesAsync(context, instance, cancellationToken, propertyName);
}
}

private async ValueTask StoreAppliedValuesAsync(ActivityExecutionContext context, IActivity activity, CancellationToken cancellationToken)
{
await StoreAppliedObjectValuesAsync(context, activity, cancellationToken);
}

private async ValueTask StoreAppliedObjectValuesAsync(ActivityExecutionContext context, object activity, CancellationToken cancellationToken, string parentName = null)
{
var properties = activity.GetType().GetProperties().Where(IsActivityProperty).ToList();
var nestedProperties = activity.GetType().GetProperties().Where(IsActivityObjectProperty).ToList();
var propertyStorageProviderDictionary = context.ActivityBlueprint.PropertyStorageProviders;
var workflowStorageContext = new WorkflowStorageContext(context.WorkflowInstance, context.ActivityId);

foreach (var property in properties)
{
var propertyName = parentName == null ? property.Name : $"{parentName}_{property.Name}";
var value = property.GetValue(activity);
var attr = property.GetCustomAttribute<ActivityPropertyAttributeBase>();
var providerName = propertyStorageProviderDictionary.GetItem(property.Name) ?? attr.DefaultWorkflowStorageProvider;
await _workflowStorageService.SaveAsync(providerName, workflowStorageContext, property.Name, value, cancellationToken);
var providerName = propertyStorageProviderDictionary.GetItem(propertyName) ?? attr.DefaultWorkflowStorageProvider;
await _workflowStorageService.SaveAsync(providerName, workflowStorageContext, propertyName, value, cancellationToken);
}

foreach (var nestedProperty in nestedProperties)
{
var instance = Activator.CreateInstance(nestedProperty.PropertyType);
var propertyName = parentName == null ? nestedProperty.Name : $"{parentName}_{nestedProperty.Name}";
await StoreAppliedObjectValuesAsync(context, instance, cancellationToken, propertyName);
}
}

Expand All @@ -100,9 +129,31 @@ private bool ShouldSetProperties(ActivityExecutionContext context, IActivity act
return true;
}

private IEnumerable<PropertyInfo> GetNestedProperties(object activity)
{
var properties = activity.GetType().GetProperties().Where(IsActivityProperty).ToList();
var objectProperties = activity.GetType().GetProperties().Where(IsActivityObjectProperty).ToList();

foreach (var property in properties)
{
yield return property;
}

foreach (var property in objectProperties)
{
var child = GetNestedProperties(property);

foreach (var ch in child)
{
yield return ch;
}
}
}

private bool IsReturningComposite(IActivity activity) => activity is CompositeActivity && activity.Data!.GetState<bool>(nameof(CompositeActivity.IsScheduled));
private bool IsReturningIf(IActivity activity) => activity is If && activity.Data!.GetState<bool>("Unwinding");
private bool IsReturningSwitch(IActivity activity) => activity is Switch && activity.Data!.GetState<bool>("Unwinding");
private bool IsActivityProperty(PropertyInfo property) => (property.GetCustomAttribute<ActivityInputAttribute>() != null || property.GetCustomAttribute<ActivityOutputAttribute>() != null) && property.GetCustomAttribute<NonPersistableAttribute>() == null;
private bool IsActivityObjectProperty(PropertyInfo property) => (property.GetCustomAttribute<ActivityInputObjectAttribute>() != null) && property.GetCustomAttribute<NonPersistableAttribute>() == null;
}
}

0 comments on commit c4bade7

Please sign in to comment.