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:
Sipke Schoorstra 2024-01-17 22:11:03 +01:00
parent 39788fb8eb
commit 18fd4ff796
22 changed files with 420 additions and 96 deletions

View file

@ -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}

View file

@ -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)

View file

@ -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);

View file

@ -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)

View file

@ -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);
}

View file

@ -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>

View file

@ -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();

View file

@ -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;
}
}
}

View file

@ -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);

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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>

View file

@ -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; }
}

View file

@ -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; }
}

View file

@ -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();

View file

@ -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"
}
}
}
}

View file

@ -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!")
}
};
}
}

View file

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.EntityFrameworkCore": "Warning",
"Microsoft.AspNetCore": "Warning"
}
}
}

View file

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.EntityFrameworkCore": "Warning",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View file

@ -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" />

View file

@ -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);
};
});