Search code examples
c#.netelsa-workflows

How to build a custom loop activity in Elsa workflows


I'm trying to make a custom activity that will eventually do a complicated database query or API call to get a bunch of records and loop over them. I'm sure it could be done with the built in flow control activities, but I want to make this usable by non-programmers who don't know or care what a foreach loop is, so putting a lot of functionality into one box is good.

My first attempt was to inherit from ForEach and do some initialization before letting OnExecute do its thing, but the result feels somewhat hacky.

public class FancyForEach : ForEach
{
    private bool? Initialized
    {
        get
        {
            return GetState<bool?>("Initialized");
        }
        set
        {
            SetState(value, "Initialized");
        }
    }

    protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context)
    {
        if (Initialized != true)
        {
            Items = GetThingsFromDatabase();
            Initialized = true;
        }

        return base.OnExecute(context);
    }

    protected List<DatabaseThings> GetThingsFromDatabase()
    {
        // Fancy stuff here, including paging eventually.
    }
}

It seems like it would be a little cleaner to instantiate a ForEach somewhere within the activity rather than inherit from it, but I can't puzzle out a way to make that work. I imagine a decent solution would be to trigger another workflow for each record, but I'd rather not do that, again to make this easy to digest for people who aren't programmers.

Can anyone offer a suggestion on the best way to make this work? This is my first project using Elsa, so maybe I'm approaching it from an entirely wrong direction!


Solution

  • If I understand correctly, your activity is responsible for loading in the data and looping over it, while the user of the activity should be able to specify what happens in each iteration.

    If so, then you might implement something like this:

    [Activity(
        Category = "Control Flow",
        Description = "Iterate over a collection.",
        Outcomes = new[] { OutcomeNames.Iterate, OutcomeNames.Done }
    )]
    public class FancyForEach : Activity
    {
        private bool? Initialized
        {
            get => GetState<bool?>();
            set => SetState(value);
        }
        
        private IList<DatabaseThings>? Items
        {
            get => GetState<IList<DatabaseThings>?>();
            set => SetState(value);
        }
        
        private int? CurrentIndex
        {
            get => GetState<int?>();
            set => SetState(value);
        }
        
        protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context)
        {
            if (Initialized != true)
            {
                Items = GetThingsFromDatabase();
                Initialized = true;
            }
            
            var collection = Items.ToList();
            var currentIndex = CurrentIndex ?? 0;
    
            if (currentIndex < collection.Count)
            {
                var currentValue = collection[currentIndex];
                var scope = context.CreateScope();
    
                scope.Variables.Set("CurrentIndex", currentIndex);
                scope.Variables.Set("CurrentValue", currentValue);
    
                CurrentIndex = currentIndex + 1;
                context.JournalData.Add("Current Index", currentIndex);
    
                // For each iteration, return an outcome to which the user can connect activities to.
                return Outcome(OutcomeNames.Iterate, currentValue);
            }
    
            CurrentIndex = null;
            return Done();
        }
        
        protected List<DatabaseThings> GetThingsFromDatabase()
        {
            // Fancy stuff here, including paging eventually.
        }
    }
    

    This example loads the database items into memory once and then stores this list in workflow state (via Items) - which may or may not be desirable, since this has the potential of increasing the size of the workflow instance significantly depending on the size of each record and number of records.

    A more scalable approach would be to load just one item per iteration, keeping track of the current index that was loaded, incrementing it (i.e. pagination with a page size of 1).