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.
This commit is contained in:
parent
39788fb8eb
commit
18fd4ff796
7
Elsa.sln
7
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}
|
||||
|
|
|
|||
|
|
@ -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<object?>)(() => input.Value));
|
||||
foreach (var inputDefinition in inputDefinitions)
|
||||
{
|
||||
var input = inputs.GetValueOrDefault(inputDefinition.Name);
|
||||
engine.SetValue($"get{inputDefinition.Name}", (Func<object?>)(() => input?.Value));
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateVariableAccessors(Engine engine, ExpressionExecutionContext context)
|
||||
|
|
|
|||
|
|
@ -47,7 +47,13 @@ public class ForEach<T> : Activity
|
|||
/// The set of values to iterate.
|
||||
/// </summary>
|
||||
[Input(Description = "The set of values to iterate.")]
|
||||
public Input<ICollection<T>> Items { get; set; } = new(Array.Empty<T>());
|
||||
public Input<ICollection<T>>? Items { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The source of values to iterate.
|
||||
/// </summary>
|
||||
[Input(Description = "The set of values to iterate.")]
|
||||
public Input<IAsyncEnumerable<T>>? ItemSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The activity to execute for each iteration.
|
||||
|
|
@ -79,15 +85,16 @@ public class ForEach<T> : Activity
|
|||
}
|
||||
|
||||
var currentIndex = context.GetProperty<int>(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<T> : Activity
|
|||
context.UpdateProperty<int>(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);
|
||||
|
|
|
|||
|
|
@ -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<T> : Activity
|
|||
/// The items to iterate.
|
||||
/// </summary>
|
||||
[Input(Description = "The items to iterate through.")]
|
||||
public Input<ICollection<T>> Items { get; set; } = new(Array.Empty<T>());
|
||||
public Input<object> Items { get; set; } = new(Array.Empty<T>());
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IActivity"/> to execute each iteration.
|
||||
|
|
@ -39,17 +40,12 @@ public class ParallelForEach<T> : Activity
|
|||
/// <inheritdoc />
|
||||
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
|
||||
{
|
||||
var items = context.Get(Items)!.ToList();
|
||||
var items = context.GetItemSource<T>(Items);
|
||||
var tags = new List<Guid>();
|
||||
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<T>("CurrentValue", item)
|
||||
|
|
@ -57,17 +53,9 @@ public class ParallelForEach<T> : 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<int>("CurrentIndex", currentIndex++)
|
||||
{
|
||||
StorageDriverType = typeof(WorkflowStorageDriver)
|
||||
};
|
||||
|
||||
var variables = new List<Variable>
|
||||
{
|
||||
currentValueVariable,
|
||||
currentIndexVariable
|
||||
};
|
||||
var currentIndexVariable = new Variable<int>("CurrentIndex", currentIndex++) { StorageDriverType = typeof(WorkflowStorageDriver) };
|
||||
var variables = new List<Variable> { currentValueVariable, currentIndexVariable };
|
||||
|
||||
// Schedule a body of work for each item.
|
||||
var tag = Guid.NewGuid();
|
||||
|
|
@ -77,6 +65,10 @@ public class ParallelForEach<T> : Activity
|
|||
|
||||
context.SetProperty(ScheduledTagsProperty, tags);
|
||||
context.SetProperty(CompletedTagsProperty, new List<Guid>());
|
||||
|
||||
// If there were no items, we're done.
|
||||
if (tags.Count == 0)
|
||||
await context.CompleteActivityAsync();
|
||||
}
|
||||
|
||||
private async ValueTask OnChildCompleted(ActivityCompletedContext context)
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
namespace Elsa.Workflows.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a data source for an activity.
|
||||
/// </summary>
|
||||
public interface IActivityDataSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the data for the specified workflow execution context.
|
||||
/// </summary>
|
||||
/// <param name="context">The activity execution context.</param>
|
||||
/// <returns>An enumerable of objects.</returns>
|
||||
IAsyncEnumerable<object> GetDataAsync(ActivityExecutionContext context);
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@
|
|||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
|
||||
<PackageReference Include="ShortGuid" Version="2.0.1"/>
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="6.0.0-preview.4.21253.7"/>
|
||||
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
using System.Collections;
|
||||
using Elsa.Workflows.Models;
|
||||
|
||||
namespace Elsa.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for the ActivityExecutionContext class.
|
||||
/// </summary>
|
||||
public static class ItemSourceActivityExecutionContextExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves the item source and returns it as an asynchronous enumerable.
|
||||
/// Supported types are <see cref="IEnumerable{T}"/>, <see cref="IAsyncEnumerable{T}"/> and <see> <cref>IAsyncEnumerable{IEnumerable{T}}</cref></see>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the items in the source collection.</typeparam>
|
||||
/// <param name="context">The activity execution context.</param>
|
||||
/// <param name="input">The input object.</param>
|
||||
/// <returns>An asynchronous enumerable of items from the source collection.</returns>
|
||||
public static async IAsyncEnumerable<T> GetItemSource<T>(this ActivityExecutionContext context, Input<object> 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<IEnumerable<T>> typedItems)
|
||||
await foreach (var typedItem in typedItems)
|
||||
foreach (T item in typedItem)
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
if (items is IEnumerable<T> enumerable)
|
||||
{
|
||||
foreach (T item in enumerable)
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -48,13 +48,7 @@ public class BulkDispatchWorkflows : Activity
|
|||
/// The data source to use for dispatching the workflows.
|
||||
/// </summary>
|
||||
[Input(Description = "The data source to use for dispatching the workflows.")]
|
||||
public Input<ICollection<object>>? Items { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The data source to use for dispatching the workflows.
|
||||
/// </summary>
|
||||
[Input(Description = "The data source to use for dispatching the workflows.")]
|
||||
public IActivityDataSource? DataSource { get; set; }
|
||||
public Input<object> Items { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 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<object>(Items);
|
||||
var dispatchedInstancesCount = 0L;
|
||||
var batchSize = 1000;
|
||||
var batch = new List<object>();
|
||||
|
||||
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<object> 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<string> DispatchChildWorkflowAsync(ActivityExecutionContext context, object item)
|
||||
|
|
@ -206,14 +178,7 @@ public class BulkDispatchWorkflows : Activity
|
|||
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
private IAsyncEnumerable<object> 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;
|
||||
|
|
|
|||
|
|
@ -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<IAsyncEnumerable<ICollection<Order>>>
|
||||
{
|
||||
/// <summary>
|
||||
/// The total number of orders to fetch.
|
||||
/// </summary>
|
||||
[Input(Description = "The total number of orders to fetch.")]
|
||||
public Input<int> Count { get; set; } = new(100);
|
||||
|
||||
/// <summary>
|
||||
/// The number of orders to fetch per batch.
|
||||
/// </summary>
|
||||
[Input(Description = "The number of orders to fetch per batch.")]
|
||||
public Input<int> 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<Order> GenerateOrders(int count)
|
||||
{
|
||||
var orderFaker = new Faker<Order>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ICollection<Product>>
|
||||
{
|
||||
private const string CurrentBathKey = nameof(CurrentBathKey);
|
||||
|
||||
/// <summary>
|
||||
/// The total number of products to fetch.
|
||||
/// </summary>
|
||||
[Input(Description = "The total number of products to fetch.")]
|
||||
public Input<int> Count { get; set; } = new(100);
|
||||
|
||||
/// <summary>
|
||||
/// The number of products to fetch per batch.
|
||||
/// </summary>
|
||||
[Input(Description = "The number of products to fetch per batch.")]
|
||||
public Input<int> 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<string, object>
|
||||
{
|
||||
[CurrentBathKey] = currentBatch
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Complete the activity.
|
||||
await context.CompleteActivityAsync();
|
||||
}
|
||||
|
||||
private IEnumerable<Product> GenerateProducts(int count)
|
||||
{
|
||||
var productFaker = new Faker<Product>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\bundles\Elsa\Elsa.csproj" />
|
||||
<ProjectReference Include="..\..\..\modules\Elsa.CSharp\Elsa.CSharp.csproj" />
|
||||
<ProjectReference Include="..\..\..\modules\Elsa.EntityFrameworkCore.Sqlite\Elsa.EntityFrameworkCore.Sqlite.csproj" />
|
||||
<ProjectReference Include="..\..\..\modules\Elsa.EntityFrameworkCore\Elsa.EntityFrameworkCore.csproj" />
|
||||
<ProjectReference Include="..\..\..\modules\Elsa.Http\Elsa.Http.csproj" />
|
||||
<ProjectReference Include="..\..\..\modules\Elsa.Identity\Elsa.Identity.csproj" />
|
||||
<ProjectReference Include="..\..\..\modules\Elsa.Scheduling\Elsa.Scheduling.csproj" />
|
||||
<ProjectReference Include="..\..\..\modules\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bogus" Version="35.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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<Program>();
|
||||
elsa.AddWorkflowsFrom<Program>();
|
||||
});
|
||||
|
||||
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();
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IAsyncEnumerable<Order>>();
|
||||
builder.Root = new Sequence
|
||||
{
|
||||
Activities =
|
||||
{
|
||||
new WriteLine("Fetching orders..."),
|
||||
new FetchOrders(),
|
||||
new ForEach<Order>
|
||||
{
|
||||
ItemSource = new(orders)
|
||||
},
|
||||
new WriteLine("Done!")
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.EntityFrameworkCore": "Warning",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.EntityFrameworkCore": "Warning",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\bundles\Elsa\Elsa.csproj" />
|
||||
<ProjectReference Include="..\..\..\modules\Elsa.EntityFrameworkCore.Sqlite\Elsa.EntityFrameworkCore.Sqlite.csproj" />
|
||||
<ProjectReference Include="..\..\..\modules\Elsa.EntityFrameworkCore\Elsa.EntityFrameworkCore.csproj" />
|
||||
<ProjectReference Include="..\..\..\modules\Elsa.Http\Elsa.Http.csproj" />
|
||||
<ProjectReference Include="..\..\..\modules\Elsa.Identity\Elsa.Identity.csproj" />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue