From 18fd4ff796432793d76f3b4b97f70de2badffb2b Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 17 Jan 2024 22:11:03 +0100 Subject: [PATCH] Add BatchProcessing sample and update ForEach activities A new BatchProcessing sample project has been added demonstrating how to process items in batches. ForEach and ParallelForEach activities were updated to allow using an IAsyncEnumerable as source. BulkDispatchWorkflows was also refactored to improve its efficiency and handling. --- Elsa.sln | 7 ++ .../Services/JintJavaScriptEvaluator.cs | 10 ++- .../Activities/ForEachT.cs | 47 ++++++++++-- .../Activities/ParallelForEachT.cs | 30 +++----- .../Contracts/IActivityDataSource.cs | 14 ---- .../Elsa.Workflows.Core.csproj | 1 + .../ExpressionExecutionContextExtensions.cs | 4 ++ ...ourceActivityExecutionContextExtensions.cs | 46 ++++++++++++ .../DefaultWorkSchedulerMiddleware.cs | 3 +- .../Activities/BulkDispatchWorkflows.cs | 71 +++++-------------- .../Activities/FetchOrders.cs | 46 ++++++++++++ .../Activities/FetchProducts.cs | 65 +++++++++++++++++ ...Elsa.Samples.AspNet.BatchProcessing.csproj | 24 +++++++ .../Models/Order.cs | 10 +++ .../Models/Product.cs | 8 +++ .../Program.cs | 42 +++++++++++ .../Properties/launchSettings.json | 38 ++++++++++ .../Workflows/OrderBatchProcessor.cs | 28 ++++++++ .../appsettings.Development.json | 9 +++ .../appsettings.json | 10 +++ .../Elsa.Samples.AspNet.WorkflowServer.csproj | 1 + .../Program.cs | 2 +- 22 files changed, 420 insertions(+), 96 deletions(-) delete mode 100644 src/modules/Elsa.Workflows.Core/Contracts/IActivityDataSource.cs create mode 100644 src/modules/Elsa.Workflows.Core/Extensions/ItemSourceActivityExecutionContextExtensions.cs create mode 100644 src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Activities/FetchOrders.cs create mode 100644 src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Activities/FetchProducts.cs create mode 100644 src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Elsa.Samples.AspNet.BatchProcessing.csproj create mode 100644 src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Models/Order.cs create mode 100644 src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Models/Product.cs create mode 100644 src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Program.cs create mode 100644 src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Properties/launchSettings.json create mode 100644 src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Workflows/OrderBatchProcessor.cs create mode 100644 src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/appsettings.Development.json create mode 100644 src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/appsettings.json diff --git a/Elsa.sln b/Elsa.sln index 5af513753..f873a4d89 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -308,6 +308,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Studio.Web", "src\bund EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Samples.AspNet.CustomUIHandler", "src\samples\aspnet\Elsa.Samples.AspNet.CustomUIHandler\Elsa.Samples.AspNet.CustomUIHandler.csproj", "{832D6FF5-16D3-41D1-8FDD-D9BEC26BDA6A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.AspNet.BatchProcessing", "src\samples\aspnet\Elsa.Samples.AspNet.BatchProcessing\Elsa.Samples.AspNet.BatchProcessing.csproj", "{0AAF5EF6-02E5-44F9-B2CB-B1401FC5EF66}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -750,6 +752,10 @@ Global {832D6FF5-16D3-41D1-8FDD-D9BEC26BDA6A}.Debug|Any CPU.Build.0 = Debug|Any CPU {832D6FF5-16D3-41D1-8FDD-D9BEC26BDA6A}.Release|Any CPU.ActiveCfg = Release|Any CPU {832D6FF5-16D3-41D1-8FDD-D9BEC26BDA6A}.Release|Any CPU.Build.0 = Release|Any CPU + {0AAF5EF6-02E5-44F9-B2CB-B1401FC5EF66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0AAF5EF6-02E5-44F9-B2CB-B1401FC5EF66}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0AAF5EF6-02E5-44F9-B2CB-B1401FC5EF66}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0AAF5EF6-02E5-44F9-B2CB-B1401FC5EF66}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -884,6 +890,7 @@ Global {26888832-DBEA-4B23-8DC2-84478A702CC4} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5} {D5C149EE-276C-4C59-98F9-37F0EA2A7866} = {F06B9573-DF68-4606-866C-A7546A10A05A} {832D6FF5-16D3-41D1-8FDD-D9BEC26BDA6A} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5} + {0AAF5EF6-02E5-44F9-B2CB-B1401FC5EF66} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E} diff --git a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs index 53adfb070..4c56e5fe5 100644 --- a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs +++ b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs @@ -116,10 +116,14 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator if (context.IsInsideCompositeActivity()) return; - var inputs = context.GetWorkflowInputs(); + var inputs = context.GetWorkflowInputs().ToDictionary(x => x.Name); + var inputDefinitions = context.GetWorkflowExecutionContext().Workflow.Inputs; - foreach (var input in inputs) - engine.SetValue($"get{input.Name}", (Func)(() => input.Value)); + foreach (var inputDefinition in inputDefinitions) + { + var input = inputs.GetValueOrDefault(inputDefinition.Name); + engine.SetValue($"get{inputDefinition.Name}", (Func)(() => input?.Value)); + } } private static void CreateVariableAccessors(Engine engine, ExpressionExecutionContext context) diff --git a/src/modules/Elsa.Workflows.Core/Activities/ForEachT.cs b/src/modules/Elsa.Workflows.Core/Activities/ForEachT.cs index c954f50b7..1ef5e49ea 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/ForEachT.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/ForEachT.cs @@ -47,7 +47,13 @@ public class ForEach : Activity /// The set of values to iterate. /// [Input(Description = "The set of values to iterate.")] - public Input> Items { get; set; } = new(Array.Empty()); + public Input>? Items { get; set; } + + /// + /// The source of values to iterate. + /// + [Input(Description = "The set of values to iterate.")] + public Input>? ItemSource { get; set; } /// /// The activity to execute for each iteration. @@ -79,15 +85,16 @@ public class ForEach : Activity } var currentIndex = context.GetProperty(CurrentIndexProperty); - var items = context.Get(Items)!.ToList(); + var currentValueTuple = await GetCurrentValueAsync(context, currentIndex); - if (currentIndex >= items.Count) + if (!currentValueTuple.Exists) { await context.CompleteActivityAsync(); return; } - - var currentValue = items[currentIndex]; + + var currentValue = currentValueTuple.Value; + context.Set(CurrentValue, currentValue); if (Body != null) @@ -106,6 +113,36 @@ public class ForEach : Activity context.UpdateProperty(CurrentIndexProperty, x => x + 1); } + private async Task<(T Value, bool Exists)> GetCurrentValueAsync(ActivityExecutionContext context, int currentIndex) + { + var items = context.Get(Items)?.ToList(); + + if (items != null) + { + return (currentIndex >= items.Count ? (default, false) : (items[currentIndex], true))!; + } + + var itemSource = context.Get(ItemSource); + + if(itemSource != null) + { + await using var enumerator = itemSource.GetAsyncEnumerator(); + + // Move the cursor to the current index. + for (var i = 0; i < currentIndex; i++) + await enumerator.MoveNextAsync(); + + var hasNext = await enumerator.MoveNextAsync(); + + if(!hasNext) + return (default, false)!; + + return (enumerator.Current, true); + } + + return (default, false)!; + } + private async ValueTask OnChildCompleted(ActivityCompletedContext context) { await HandleIteration(context.TargetContext); diff --git a/src/modules/Elsa.Workflows.Core/Activities/ParallelForEachT.cs b/src/modules/Elsa.Workflows.Core/Activities/ParallelForEachT.cs index ac8c437bc..07cee0097 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/ParallelForEachT.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/ParallelForEachT.cs @@ -1,3 +1,4 @@ +using System.Collections; using System.Runtime.CompilerServices; using Elsa.Expressions.Helpers; using Elsa.Extensions; @@ -28,7 +29,7 @@ public class ParallelForEach : Activity /// The items to iterate. /// [Input(Description = "The items to iterate through.")] - public Input> Items { get; set; } = new(Array.Empty()); + public Input Items { get; set; } = new(Array.Empty()); /// /// The to execute each iteration. @@ -39,17 +40,12 @@ public class ParallelForEach : Activity /// protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { - var items = context.Get(Items)!.ToList(); + var items = context.GetItemSource(Items); var tags = new List(); var currentIndex = 0; - if (items.Count == 0) - { - await context.CompleteActivityAsync(); - return; - } - - foreach (var item in items) + // Iterate over the items. + await foreach (var item in items) { // For each item, declare a new variable for the work to be scheduled. var currentValueVariable = new Variable("CurrentValue", item) @@ -57,17 +53,9 @@ public class ParallelForEach : Activity // TODO: This should be configurable, because this won't work for e.g. file streams and other non-serializable types. StorageDriverType = typeof(WorkflowStorageDriver) }; - - var currentIndexVariable = new Variable("CurrentIndex", currentIndex++) - { - StorageDriverType = typeof(WorkflowStorageDriver) - }; - var variables = new List - { - currentValueVariable, - currentIndexVariable - }; + var currentIndexVariable = new Variable("CurrentIndex", currentIndex++) { StorageDriverType = typeof(WorkflowStorageDriver) }; + var variables = new List { currentValueVariable, currentIndexVariable }; // Schedule a body of work for each item. var tag = Guid.NewGuid(); @@ -77,6 +65,10 @@ public class ParallelForEach : Activity context.SetProperty(ScheduledTagsProperty, tags); context.SetProperty(CompletedTagsProperty, new List()); + + // If there were no items, we're done. + if (tags.Count == 0) + await context.CompleteActivityAsync(); } private async ValueTask OnChildCompleted(ActivityCompletedContext context) diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IActivityDataSource.cs b/src/modules/Elsa.Workflows.Core/Contracts/IActivityDataSource.cs deleted file mode 100644 index 9059a9644..000000000 --- a/src/modules/Elsa.Workflows.Core/Contracts/IActivityDataSource.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Elsa.Workflows.Contracts; - -/// -/// Represents a data source for an activity. -/// -public interface IActivityDataSource -{ - /// - /// Gets the data for the specified workflow execution context. - /// - /// The activity execution context. - /// An enumerable of objects. - IAsyncEnumerable GetDataAsync(ActivityExecutionContext context); -} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj b/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj index 6dbf5d73e..1b47908ce 100644 --- a/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj +++ b/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj @@ -34,6 +34,7 @@ + diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs index cb9dd853c..882883686 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs @@ -476,6 +476,10 @@ public static class ExpressionExecutionContextExtensions if (obj is not IEnumerable enumerable || obj is string || obj is IDictionary) return obj; + // If this is an async enumerable, return as-is. + if (obj.GetType().Name == "AsyncIListEnumerableAdapter`1") + return obj; + // Use LINQ to convert the IEnumerable to an array. var elementType = obj.GetType().GetGenericArguments().FirstOrDefault(); diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ItemSourceActivityExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ItemSourceActivityExecutionContextExtensions.cs new file mode 100644 index 000000000..66fd31869 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Extensions/ItemSourceActivityExecutionContextExtensions.cs @@ -0,0 +1,46 @@ +using System.Collections; +using Elsa.Workflows.Models; + +namespace Elsa.Workflows; + +/// +/// Provides extension methods for the ActivityExecutionContext class. +/// +public static class ItemSourceActivityExecutionContextExtensions +{ + /// + /// Retrieves the item source and returns it as an asynchronous enumerable. + /// Supported types are , and IAsyncEnumerable{IEnumerable{T}}. + /// + /// The type of the items in the source collection. + /// The activity execution context. + /// The input object. + /// An asynchronous enumerable of items from the source collection. + public static async IAsyncEnumerable GetItemSource(this ActivityExecutionContext context, Input input) + { + var items = context.Get(input); + + if (items == null) + yield break; + + var itemsType = items.GetType(); + if (itemsType.Name == "AsyncEnumerableAdapter`1") + { + var isBatch = itemsType.GenericTypeArguments.Length == 1 && typeof(IEnumerable).IsAssignableFrom(itemsType.GenericTypeArguments[0]); + + if (isBatch) + { + if(items is IAsyncEnumerable> typedItems) + await foreach (var typedItem in typedItems) + foreach (T item in typedItem) + yield return item; + } + } + + if (items is IEnumerable enumerable) + { + foreach (T item in enumerable) + yield return item; + } + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultWorkSchedulerMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultWorkSchedulerMiddleware.cs index 5d16919b4..d8b6313b1 100644 --- a/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultWorkSchedulerMiddleware.cs +++ b/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultWorkSchedulerMiddleware.cs @@ -56,7 +56,8 @@ public class DefaultActivitySchedulerMiddleware : WorkflowExecutionMiddleware Owner = workItem.Owner, ExistingActivityExecutionContext = workItem.ExistingActivityExecutionContext, Tag = workItem.Tag, - Variables = workItem.Variables + Variables = workItem.Variables, + Input = workItem.Input }; await _activityInvoker.InvokeAsync(context, workItem.Activity, options); diff --git a/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs b/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs index 25578afd2..8bab6daec 100644 --- a/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs +++ b/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs @@ -48,13 +48,7 @@ public class BulkDispatchWorkflows : Activity /// The data source to use for dispatching the workflows. /// [Input(Description = "The data source to use for dispatching the workflows.")] - public Input>? Items { get; set; } - - /// - /// The data source to use for dispatching the workflows. - /// - [Input(Description = "The data source to use for dispatching the workflows.")] - public IActivityDataSource? DataSource { get; set; } + public Input Items { get; set; } = default!; /// /// The correlation ID to associate the workflow with. @@ -95,30 +89,15 @@ public class BulkDispatchWorkflows : Activity protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { var waitForCompletion = WaitForCompletion.GetOrDefault(context); - var items = GetItemsAsync(context).WithCancellation(context.CancellationToken); + var items = context.GetItemSource(Items); var dispatchedInstancesCount = 0L; - var batchSize = 1000; - var batch = new List(); await foreach (var item in items) { - batch.Add(item); - - if (batch.Count < batchSize) - continue; - - await ProcessBatch(context, batch); - dispatchedInstancesCount += batch.Count; - batch.Clear(); + await ProcessItem(context, item); + dispatchedInstancesCount++; } - - // Process the last batch if it has any items. - if (batch.Count > 0) - { - await ProcessBatch(context, batch); - dispatchedInstancesCount += batch.Count; - } - + context.SetProperty(DispatchedInstancesCountKey, dispatchedInstancesCount); // If we need to wait for the child workflow to complete, create a bookmark. @@ -142,29 +121,22 @@ public class BulkDispatchWorkflows : Activity // Otherwise, we can complete immediately. await context.CompleteActivityAsync(); } - - // Cancelling children } - private async Task ProcessBatch(ActivityExecutionContext context, List items) + private async Task ProcessItem(ActivityExecutionContext context, object item) { - var tasks = items.Select(async item => + try { - try - { - await DispatchChildWorkflowAsync(context, item); - } - catch (TaskCanceledException) - { - await context.CompleteActivityWithOutcomesAsync("Canceled"); - } - catch (Exception ex) - { - context.JournalData.Add("Error", ex.Message); - } - }); - - await Task.WhenAll(tasks); + await DispatchChildWorkflowAsync(context, item); + } + catch (TaskCanceledException) + { + await context.CompleteActivityWithOutcomesAsync("Canceled"); + } + catch (Exception ex) + { + context.JournalData.Add("Error", ex.Message); + } } private async ValueTask DispatchChildWorkflowAsync(ActivityExecutionContext context, object item) @@ -206,14 +178,7 @@ public class BulkDispatchWorkflows : Activity return instanceId; } - - private IAsyncEnumerable GetItemsAsync(ActivityExecutionContext context) - { - var items = Items.Get(context); - var dataSource = DataSource != null ? DataSource.GetDataAsync(context) : items.ToAsyncEnumerable(); - return dataSource; - } - + private async ValueTask OnChildWorkflowCompletedAsync(ActivityExecutionContext context) { var input = context.WorkflowInput; diff --git a/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Activities/FetchOrders.cs b/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Activities/FetchOrders.cs new file mode 100644 index 000000000..9736e1b93 --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Activities/FetchOrders.cs @@ -0,0 +1,46 @@ +using Bogus; +using Elsa.Extensions; +using Elsa.Samples.AspNet.BatchProcessing.Models; +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; + +namespace Elsa.Samples.AspNet.BatchProcessing.Activities; + +[Activity("Demo", "Warehousing", "Fetch orders from the data source.")] +[Output(IsSerializable = false)] +public class FetchOrders : CodeActivity>> +{ + /// + /// The total number of orders to fetch. + /// + [Input(Description = "The total number of orders to fetch.")] + public Input Count { get; set; } = new(100); + + /// + /// The number of orders to fetch per batch. + /// + [Input(Description = "The number of orders to fetch per batch.")] + public Input BatchSize { get; set; } = new(100); + + protected override void Execute(ActivityExecutionContext context) + { + var count = Count.Get(context); + var batchSize = BatchSize.Get(context); + var orders = GenerateOrders(count).Chunk(batchSize).ToAsyncEnumerable(); + + Result.Set(context, orders); + } + + private IEnumerable GenerateOrders(int count) + { + var orderFaker = new Faker() + .RuleFor(o => o.Id, f => Guid.NewGuid().ToString()) + .RuleFor(o => o.CustomerId, f => Guid.NewGuid().ToString()) + .RuleFor(o => o.ProductId, f => Guid.NewGuid().ToString()) + .RuleFor(o => o.Quantity, f => f.Random.Int(1, 100)) + .RuleFor(o => o.Price, f => f.Random.Decimal(0.01m, 1000.00m)); + + return orderFaker.Generate(count); + } +} \ No newline at end of file diff --git a/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Activities/FetchProducts.cs b/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Activities/FetchProducts.cs new file mode 100644 index 000000000..f54dcb9fb --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Activities/FetchProducts.cs @@ -0,0 +1,65 @@ +using Bogus; +using Elsa.Extensions; +using Elsa.Samples.AspNet.BatchProcessing.Models; +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using Elsa.Workflows.Options; + +namespace Elsa.Samples.AspNet.BatchProcessing.Activities; + +[Activity("Demo", "Warehousing", "Fetch products from the data source.")] +[Output(IsSerializable = false)] +public class FetchProducts : CodeActivity> +{ + private const string CurrentBathKey = nameof(CurrentBathKey); + + /// + /// The total number of products to fetch. + /// + [Input(Description = "The total number of products to fetch.")] + public Input Count { get; set; } = new(100); + + /// + /// The number of products to fetch per batch. + /// + [Input(Description = "The number of products to fetch per batch.")] + public Input BatchSize { get; set; } = new(100); + + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var count = Count.Get(context); + var batchSize = BatchSize.Get(context); + var currentBatch = context.ActivityInput.TryGetValue(CurrentBathKey, out var currentBatchValue) ? (int)currentBatchValue : 0; + var orders = GenerateProducts(count).Skip(currentBatch * batchSize).Take(batchSize).ToList(); + + if (orders.Any()) + { + currentBatch++; + context.SetProperty(CurrentBathKey, currentBatch); + Result.Set(context, orders); + + // Schedule the next batch. + await context.ScheduleActivityAsync(this, new ScheduleWorkOptions + { + Input = new Dictionary + { + [CurrentBathKey] = currentBatch + } + }); + } + + // Complete the activity. + await context.CompleteActivityAsync(); + } + + private IEnumerable GenerateProducts(int count) + { + var productFaker = new Faker() + .RuleFor(o => o.Id, f => Guid.NewGuid().ToString()) + .RuleFor(o => o.Name, f => Guid.NewGuid().ToString()) + .RuleFor(o => o.Price, f => f.Random.Decimal(0.01m, 1000.00m)); + + return productFaker.Generate(count); + } +} \ No newline at end of file diff --git a/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Elsa.Samples.AspNet.BatchProcessing.csproj b/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Elsa.Samples.AspNet.BatchProcessing.csproj new file mode 100644 index 000000000..8f87493f0 --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Elsa.Samples.AspNet.BatchProcessing.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Models/Order.cs b/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Models/Order.cs new file mode 100644 index 000000000..c5c4bd438 --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Models/Order.cs @@ -0,0 +1,10 @@ +namespace Elsa.Samples.AspNet.BatchProcessing.Models; + +public class Order +{ + public string Id { get; set; } = default!; + public string CustomerId { get; set; } = default!; + public string ProductId { get; set; } = default!; + public int Quantity { get; set; } + public decimal Price { get; set; } +} \ No newline at end of file diff --git a/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Models/Product.cs b/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Models/Product.cs new file mode 100644 index 000000000..af72291c9 --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Models/Product.cs @@ -0,0 +1,8 @@ +namespace Elsa.Samples.AspNet.BatchProcessing.Models; + +public class Product +{ + public string Id { get; set; } = default!; + public string Name { get; set; } = default!; + public decimal Price { get; set; } +} \ No newline at end of file diff --git a/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Program.cs b/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Program.cs new file mode 100644 index 000000000..53681d835 --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Program.cs @@ -0,0 +1,42 @@ +using Elsa.EntityFrameworkCore.Modules.Management; +using Elsa.EntityFrameworkCore.Modules.Runtime; +using Elsa.Extensions; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddElsa(elsa => +{ + elsa.UseWorkflowManagement(management => management.UseEntityFrameworkCore()); + elsa.UseWorkflowRuntime(runtime => runtime.UseEntityFrameworkCore()); + elsa.UseWorkflowsApi(); + elsa.UseHttp(); + elsa.UseScheduling(); + elsa.UseJavaScript(javaScript => javaScript.AllowClrAccess = true); + elsa.UseCSharp(); + elsa.UseLiquid(); + + elsa.UseIdentity(identity => + { + identity.UseAdminUserProvider(); + identity.TokenOptions = options => + { + options.SigningKey = "super-secret-tamper-free-token-signing-key"; + options.AccessTokenLifetime = TimeSpan.FromDays(1); + }; + }); + + elsa.UseDefaultAuthentication(auth => auth.UseAdminApiKey()); + elsa.AddActivitiesFrom(); + elsa.AddWorkflowsFrom(); +}); + +builder.Services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod().WithExposedHeaders("*"))); + +var app = builder.Build(); +app.UseHttpsRedirection(); +app.UseCors(); +app.UseAuthentication(); +app.UseAuthorization(); +app.UseWorkflowsApi(); +app.UseWorkflows(); +app.Run(); \ No newline at end of file diff --git a/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Properties/launchSettings.json b/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Properties/launchSettings.json new file mode 100644 index 000000000..a82352f12 --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:14013", + "sslPort": 44367 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5210", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7024;http://localhost:5210", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Workflows/OrderBatchProcessor.cs b/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Workflows/OrderBatchProcessor.cs new file mode 100644 index 000000000..8d0df87ca --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/Workflows/OrderBatchProcessor.cs @@ -0,0 +1,28 @@ +using Elsa.Samples.AspNet.BatchProcessing.Activities; +using Elsa.Samples.AspNet.BatchProcessing.Models; +using Elsa.Workflows; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Contracts; + +namespace Elsa.Samples.AspNet.BatchProcessing.Workflows; + +public class OrderBatchProcessor : WorkflowBase +{ + protected override void Build(IWorkflowBuilder builder) + { + var orders = builder.WithVariable>(); + builder.Root = new Sequence + { + Activities = + { + new WriteLine("Fetching orders..."), + new FetchOrders(), + new ForEach + { + ItemSource = new(orders) + }, + new WriteLine("Done!") + } + }; + } +} \ No newline at end of file diff --git a/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/appsettings.Development.json b/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/appsettings.Development.json new file mode 100644 index 000000000..4b90d5b82 --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.EntityFrameworkCore": "Warning", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/appsettings.json b/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/appsettings.json new file mode 100644 index 000000000..006192806 --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.AspNet.BatchProcessing/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.EntityFrameworkCore": "Warning", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/Elsa.Samples.AspNet.WorkflowServer.csproj b/src/samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/Elsa.Samples.AspNet.WorkflowServer.csproj index d85ccfdb3..4ffba88d0 100644 --- a/src/samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/Elsa.Samples.AspNet.WorkflowServer.csproj +++ b/src/samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/Elsa.Samples.AspNet.WorkflowServer.csproj @@ -8,6 +8,7 @@ + diff --git a/src/samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/Program.cs b/src/samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/Program.cs index 3dc0e8e53..a093abdf5 100644 --- a/src/samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/Program.cs +++ b/src/samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/Program.cs @@ -30,7 +30,7 @@ builder.Services.AddElsa(elsa => identity.UseAdminUserProvider(); identity.TokenOptions = options => { - options.SigningKey = "secret-token-signing-key"; + options.SigningKey = "super-secret-tamper-free-token-signing-key"; options.AccessTokenLifetime = TimeSpan.FromDays(1); }; });