From c33549c291bdca60e1193c92bd8d55d9f49b4cec Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 8 Mar 2022 14:40:47 +0100 Subject: [PATCH] Add WorkflowContexts module --- Elsa.sln | 7 +++ .../Builders/WorkflowDefinitionBuilder.cs | 15 ++++- .../Contracts/IWorkflowDefinitionBuilder.cs | 3 + .../Extensions/DictionaryExtensions.cs | 19 +++++-- .../Models/ExpressionExecutionContext.cs | 6 +- src/core/Elsa.Core/Models/Workflow.cs | 5 +- .../Models/WorkflowExecutionContext.cs | 11 ++++ .../Elsa.Core/Services/ActivityInvoker.cs | 6 +- .../Services/WorkflowStateSerializer.cs | 14 +++-- .../Services/WorkflowPublisher.cs | 3 +- .../Abstractions/WorkflowContextProvider.cs | 24 ++++++++ .../Contracts/IWorkflowContextProvider.cs | 21 +++++++ .../Elsa.Modules.WorkflowContexts.csproj | 13 +++++ .../WorkflowExecutionBuilderExtensions.cs | 13 +++++ .../Extensions/WorkflowExtensions.cs | 22 +++++++ .../Middleware/WorkflowContextMiddleware.cs | 57 +++++++++++++++++++ .../Models/WorkflowContext.cs | 28 +++++++++ .../Entities/WorkflowDefinition.cs | 1 + .../Mappers/WorkflowDefinitionMapper.cs | 5 +- ....cs => 20220308133708_Initial.Designer.cs} | 46 +++------------ ...2_Initial.cs => 20220308133708_Initial.cs} | 44 +++----------- .../Migrations/ElsaDbContextModelSnapshot.cs | 44 +++----------- .../WorkflowDefinitionConfiguration.cs | 2 + .../WorkflowDefinitionSerializer.cs | 13 ++++- .../Elsa.Runtime/Services/TriggerIndexer.cs | 6 +- .../Elsa.Samples.Web1.csproj | 1 + .../aspnet/Elsa.Samples.Web1/Program.cs | 3 + .../Workflows/WorkflowContextsWorkflow.cs | 43 ++++++++++++++ 28 files changed, 342 insertions(+), 133 deletions(-) create mode 100644 src/modules/Elsa.Modules.WorkflowContexts/Abstractions/WorkflowContextProvider.cs create mode 100644 src/modules/Elsa.Modules.WorkflowContexts/Contracts/IWorkflowContextProvider.cs create mode 100644 src/modules/Elsa.Modules.WorkflowContexts/Elsa.Modules.WorkflowContexts.csproj create mode 100644 src/modules/Elsa.Modules.WorkflowContexts/Extensions/WorkflowExecutionBuilderExtensions.cs create mode 100644 src/modules/Elsa.Modules.WorkflowContexts/Extensions/WorkflowExtensions.cs create mode 100644 src/modules/Elsa.Modules.WorkflowContexts/Middleware/WorkflowContextMiddleware.cs create mode 100644 src/modules/Elsa.Modules.WorkflowContexts/Models/WorkflowContext.cs rename src/persistence/Elsa.Persistence.EntityFrameworkCore.Sqlite/Migrations/{20220307110612_Initial.Designer.cs => 20220308133708_Initial.Designer.cs} (87%) rename src/persistence/Elsa.Persistence.EntityFrameworkCore.Sqlite/Migrations/{20220307110612_Initial.cs => 20220308133708_Initial.cs} (86%) create mode 100644 src/samples/aspnet/Elsa.Samples.Web1/Workflows/WorkflowContextsWorkflow.cs diff --git a/Elsa.sln b/Elsa.sln index 2c1522d54..7682d09c5 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -90,6 +90,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Modules.JavaScript", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Modules.Hangfire", "src\modules\Elsa.Modules.Hangfire\Elsa.Modules.Hangfire.csproj", "{0601A2A6-2C62-418B-9104-8CDE497E5283}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Modules.WorkflowContexts", "src\modules\Elsa.Modules.WorkflowContexts\Elsa.Modules.WorkflowContexts.csproj", "{302BFC43-ED2F-43AE-8AD4-FCD481B0AC67}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -200,6 +202,10 @@ Global {0601A2A6-2C62-418B-9104-8CDE497E5283}.Debug|Any CPU.Build.0 = Debug|Any CPU {0601A2A6-2C62-418B-9104-8CDE497E5283}.Release|Any CPU.ActiveCfg = Release|Any CPU {0601A2A6-2C62-418B-9104-8CDE497E5283}.Release|Any CPU.Build.0 = Release|Any CPU + {302BFC43-ED2F-43AE-8AD4-FCD481B0AC67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {302BFC43-ED2F-43AE-8AD4-FCD481B0AC67}.Debug|Any CPU.Build.0 = Debug|Any CPU + {302BFC43-ED2F-43AE-8AD4-FCD481B0AC67}.Release|Any CPU.ActiveCfg = Release|Any CPU + {302BFC43-ED2F-43AE-8AD4-FCD481B0AC67}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {155227F0-A33B-40AA-A4B4-06F813EB921B} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} @@ -240,5 +246,6 @@ Global {49716A83-239C-4913-BC11-E379ED2F676E} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09} {D31581AB-A6C1-4B73-AB63-45667F6C82AE} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {0601A2A6-2C62-418B-9104-8CDE497E5283} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} + {302BFC43-ED2F-43AE-8AD4-FCD481B0AC67} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} EndGlobalSection EndGlobal diff --git a/src/core/Elsa.Core/Builders/WorkflowDefinitionBuilder.cs b/src/core/Elsa.Core/Builders/WorkflowDefinitionBuilder.cs index 032c7eb63..1870983f5 100644 --- a/src/core/Elsa.Core/Builders/WorkflowDefinitionBuilder.cs +++ b/src/core/Elsa.Core/Builders/WorkflowDefinitionBuilder.cs @@ -11,6 +11,7 @@ public class WorkflowDefinitionBuilder : IWorkflowDefinitionBuilder public int Version { get; private set; } = 1; public IActivity? Root { get; private set; } public ICollection Variables { get; set; } = new List(); + public IDictionary ApplicationProperties { get; set; } = new Dictionary(); public IWorkflowDefinitionBuilder WithId(string id) { @@ -36,6 +37,18 @@ public class WorkflowDefinitionBuilder : IWorkflowDefinitionBuilder return this; } + public IWorkflowDefinitionBuilder WithVariable(Variable variable) + { + Variables.Add(variable); + return this; + } + + public IWorkflowDefinitionBuilder WithApplicationProperty(string name, object value) + { + ApplicationProperties[name] = value; + return this; + } + public Workflow BuildWorkflow() { var definitionId = DefinitionId ?? Guid.NewGuid().ToString("N"); @@ -44,6 +57,6 @@ public class WorkflowDefinitionBuilder : IWorkflowDefinitionBuilder var identity = new WorkflowIdentity(definitionId, Version, id); var publication = WorkflowPublication.LatestAndPublished; var metadata = new WorkflowMetadata(); - return new Workflow(identity, publication, metadata, root, Variables); + return new Workflow(identity, publication, metadata, root, Variables, ApplicationProperties); } } \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IWorkflowDefinitionBuilder.cs b/src/core/Elsa.Core/Contracts/IWorkflowDefinitionBuilder.cs index 5b5e0bec1..89859fb17 100644 --- a/src/core/Elsa.Core/Contracts/IWorkflowDefinitionBuilder.cs +++ b/src/core/Elsa.Core/Contracts/IWorkflowDefinitionBuilder.cs @@ -8,8 +8,11 @@ public interface IWorkflowDefinitionBuilder int Version { get; } IActivity? Root { get; } ICollection Variables { get; set; } + IDictionary ApplicationProperties { get; } IWorkflowDefinitionBuilder WithDefinitionId(string definitionId); IWorkflowDefinitionBuilder WithVersion(int version); IWorkflowDefinitionBuilder WithRoot(IActivity root); + IWorkflowDefinitionBuilder WithVariable(Variable variable); + IWorkflowDefinitionBuilder WithApplicationProperty(string name, object value); Workflow BuildWorkflow(); } \ No newline at end of file diff --git a/src/core/Elsa.Core/Extensions/DictionaryExtensions.cs b/src/core/Elsa.Core/Extensions/DictionaryExtensions.cs index e94c1e2af..ec3869311 100644 --- a/src/core/Elsa.Core/Extensions/DictionaryExtensions.cs +++ b/src/core/Elsa.Core/Extensions/DictionaryExtensions.cs @@ -4,9 +4,9 @@ namespace Elsa.Extensions; public static class DictionaryExtensions { - public static bool TryGetValue(this IReadOnlyDictionary dictionary, string key, out T value) => TryGetValue((IDictionary)dictionary, key, out value); + public static bool TryGetValue(this IReadOnlyDictionary dictionary, string key, out T? value) => TryGetValue((IDictionary)dictionary, key, out value); - public static bool TryGetValue(this IDictionary dictionary, string key, out T value) + public static bool TryGetValue(this IDictionary dictionary, string key, out T? value) { if (!dictionary.TryGetValue(key, out var item)) { @@ -18,15 +18,26 @@ public static class DictionaryExtensions return true; } - public static T GetValue(this IDictionary dictionary, string key) + public static T? GetValue(this IDictionary dictionary, string key) { return ConvertValue(dictionary[key]); } - private static T ConvertValue(object value) + public static T GetOrAdd(this IDictionary dictionary, string key, Func valueFactory) where T : notnull + { + if (dictionary.TryGetValue(key, out var value)) + return value; + + value = valueFactory(); + dictionary.Add(key, value); + return value; + } + + private static T? ConvertValue(object? value) { return value switch { + null => default, T v => v, JsonElement jsonElement => jsonElement.Deserialize()!, _ => throw new InvalidOperationException() diff --git a/src/core/Elsa.Core/Models/ExpressionExecutionContext.cs b/src/core/Elsa.Core/Models/ExpressionExecutionContext.cs index bd21d92b4..e86e9cbd0 100644 --- a/src/core/Elsa.Core/Models/ExpressionExecutionContext.cs +++ b/src/core/Elsa.Core/Models/ExpressionExecutionContext.cs @@ -6,15 +6,19 @@ public class ExpressionExecutionContext { private readonly IServiceProvider _serviceProvider; - public ExpressionExecutionContext(IServiceProvider serviceProvider, Register register, ExpressionExecutionContext? parentContext, CancellationToken cancellationToken) + public ExpressionExecutionContext(IServiceProvider serviceProvider, Register register, Workflow workflow, IDictionary transientProperties, ExpressionExecutionContext? parentContext, CancellationToken cancellationToken) { _serviceProvider = serviceProvider; Register = register; + Workflow = workflow; + TransientProperties = transientProperties; ParentContext = parentContext; CancellationToken = cancellationToken; } public Register Register { get; } + public Workflow Workflow { get; } + public IDictionary TransientProperties { get; } public ExpressionExecutionContext? ParentContext { get; set; } public CancellationToken CancellationToken { get; } diff --git a/src/core/Elsa.Core/Models/Workflow.cs b/src/core/Elsa.Core/Models/Workflow.cs index 8fad3a066..bb539f82e 100644 --- a/src/core/Elsa.Core/Models/Workflow.cs +++ b/src/core/Elsa.Core/Models/Workflow.cs @@ -7,9 +7,10 @@ public record Workflow( WorkflowPublication Publication, WorkflowMetadata Metadata, IActivity Root, - ICollection Variables) + ICollection Variables, + IDictionary ApplicationProperties) { - public static Workflow FromActivity(IActivity root) => new(WorkflowIdentity.VersionOne, WorkflowPublication.LatestDraft, new WorkflowMetadata(), root, new List()); + public static Workflow FromActivity(IActivity root) => new(WorkflowIdentity.VersionOne, WorkflowPublication.LatestDraft, new WorkflowMetadata(), root, new List(), new Dictionary()); public Workflow WithVersion(int version) => this with { Identity = Identity with { Version = version } }; public Workflow IncrementVersion() => WithVersion(Identity.Version + 1); diff --git a/src/core/Elsa.Core/Models/WorkflowExecutionContext.cs b/src/core/Elsa.Core/Models/WorkflowExecutionContext.cs index 98b443455..77a9e29f2 100644 --- a/src/core/Elsa.Core/Models/WorkflowExecutionContext.cs +++ b/src/core/Elsa.Core/Models/WorkflowExecutionContext.cs @@ -48,7 +48,18 @@ public class WorkflowExecutionContext public IActivityScheduler Scheduler { get; } public Bookmark? Bookmark { get; } public IReadOnlyDictionary Input { get; } + + /// + /// A dictionary that can be used by application code and activities to store information. Values need to be serializable, since this dictionary will be persisted alongside the workflow instance. + /// public IDictionary Properties { get; set; } = new Dictionary(); + + /// + /// A dictionary that can be used by application code and middleware to store information and even services. Values do not need to be serializable, since this dictionary will not be persisted. + /// All data will be gone once workflow execution completes. + /// + public IDictionary TransientProperties { get; set; } = new Dictionary(); + public ExecuteActivityDelegate? ExecuteDelegate { get; set; } public CancellationToken CancellationToken { get; } public IReadOnlyCollection Bookmarks => new ReadOnlyCollection(_bookmarks); diff --git a/src/core/Elsa.Core/Services/ActivityInvoker.cs b/src/core/Elsa.Core/Services/ActivityInvoker.cs index c540f8aa5..11a2b1f81 100644 --- a/src/core/Elsa.Core/Services/ActivityInvoker.cs +++ b/src/core/Elsa.Core/Services/ActivityInvoker.cs @@ -27,8 +27,10 @@ public class ActivityInvoker : IActivityInvoker // Setup an activity execution context. var register = workflowExecutionContext.Workflow.CreateRegister(); - - var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, register, parentActivityExecutionContext?.ExpressionExecutionContext, cancellationToken); + + var workflow = workflowExecutionContext.Workflow; + var transientProperties = workflowExecutionContext.TransientProperties; + var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, register, workflow, transientProperties, parentActivityExecutionContext?.ExpressionExecutionContext, cancellationToken); var activityExecutionContext = new ActivityExecutionContext(workflowExecutionContext, parentActivityExecutionContext, expressionExecutionContext, activity, cancellationToken); // Declare locations. diff --git a/src/core/Elsa.Core/Services/WorkflowStateSerializer.cs b/src/core/Elsa.Core/Services/WorkflowStateSerializer.cs index 3d9f0cb16..9e9bfa3cd 100644 --- a/src/core/Elsa.Core/Services/WorkflowStateSerializer.cs +++ b/src/core/Elsa.Core/Services/WorkflowStateSerializer.cs @@ -15,7 +15,7 @@ public class WorkflowStateSerializer : IWorkflowStateSerializer { _serviceProvider = serviceProvider; } - + public WorkflowState ReadState(WorkflowExecutionContext workflowExecutionContext) { var state = new WorkflowState @@ -44,7 +44,7 @@ public class WorkflowStateSerializer : IWorkflowStateSerializer { state.Properties = workflowExecutionContext.Properties; } - + private void SetProperties(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) { workflowExecutionContext.Properties = state.Properties; @@ -127,7 +127,9 @@ public class WorkflowStateSerializer : IWorkflowStateSerializer var cancellationToken = workflowExecutionContext.CancellationToken; var activity = workflowExecutionContext.FindActivityById(activityExecutionContextState.ScheduledActivityId); var register = new Register(activityExecutionContextState.Register.Locations); - var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, register, default, cancellationToken); + var workflow = workflowExecutionContext.Workflow; + + var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, register, workflow, new Dictionary(), default, cancellationToken); var properties = activityExecutionContextState.Properties; var activityExecutionContext = new ActivityExecutionContext(workflowExecutionContext, default, expressionExecutionContext, activity, cancellationToken) { @@ -136,7 +138,7 @@ public class WorkflowStateSerializer : IWorkflowStateSerializer }; return activityExecutionContext; } - + var activityExecutionContexts = state.ActivityExecutionContexts.Select(CreateActivityExecutionContext).ToList(); var lookup = activityExecutionContexts.ToDictionary(x => x.Id); @@ -146,10 +148,10 @@ public class WorkflowStateSerializer : IWorkflowStateSerializer var parentContext = lookup[contextState.ParentContextId!]; var contextId = contextState.Id; var context = lookup[contextId]; - context.ExpressionExecutionContext.ParentContext = parentContext.ExpressionExecutionContext; + context.ExpressionExecutionContext.ParentContext = parentContext.ExpressionExecutionContext; context.ParentActivityExecutionContext = parentContext; } - + workflowExecutionContext.ActivityExecutionContexts = activityExecutionContexts; } diff --git a/src/core/Elsa.Management/Services/WorkflowPublisher.cs b/src/core/Elsa.Management/Services/WorkflowPublisher.cs index 635d8464d..c8981f0ac 100644 --- a/src/core/Elsa.Management/Services/WorkflowPublisher.cs +++ b/src/core/Elsa.Management/Services/WorkflowPublisher.cs @@ -34,7 +34,8 @@ namespace Elsa.Management.Services WorkflowPublication.LatestDraft, new WorkflowMetadata(CreatedAt: _systemClock.UtcNow), new Sequence(), - new List()); + new List(), + new Dictionary()); } public async Task PublishAsync(string definitionId, CancellationToken cancellationToken = default) diff --git a/src/modules/Elsa.Modules.WorkflowContexts/Abstractions/WorkflowContextProvider.cs b/src/modules/Elsa.Modules.WorkflowContexts/Abstractions/WorkflowContextProvider.cs new file mode 100644 index 000000000..d67bba025 --- /dev/null +++ b/src/modules/Elsa.Modules.WorkflowContexts/Abstractions/WorkflowContextProvider.cs @@ -0,0 +1,24 @@ +using Elsa.Models; +using Elsa.Modules.WorkflowContexts.Contracts; + +namespace Elsa.Modules.WorkflowContexts.Abstractions; + +public abstract class WorkflowContextProvider : IWorkflowContextProvider +{ + async ValueTask IWorkflowContextProvider.LoadAsync(WorkflowExecutionContext workflowExecutionContext) => await LoadAsync(workflowExecutionContext); + + async ValueTask IWorkflowContextProvider.SaveAsync(WorkflowExecutionContext workflowExecutionContext, object? context) => await SaveAsync(workflowExecutionContext, (T?)context); + + protected virtual ValueTask LoadAsync(WorkflowExecutionContext workflowExecutionContext) => new(Load(workflowExecutionContext)); + protected virtual T? Load(WorkflowExecutionContext workflowExecutionContext) => default; + + protected virtual ValueTask SaveAsync(WorkflowExecutionContext workflowExecutionContext, T? context) + { + Save(workflowExecutionContext, context); + return ValueTask.CompletedTask; + } + + protected virtual void Save(WorkflowExecutionContext workflowExecutionContext, T? context) + { + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Modules.WorkflowContexts/Contracts/IWorkflowContextProvider.cs b/src/modules/Elsa.Modules.WorkflowContexts/Contracts/IWorkflowContextProvider.cs new file mode 100644 index 000000000..bfe67218e --- /dev/null +++ b/src/modules/Elsa.Modules.WorkflowContexts/Contracts/IWorkflowContextProvider.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks.Sources; +using Elsa.Models; + +namespace Elsa.Modules.WorkflowContexts.Contracts; + +/// +/// Implement this interface to implement a workflow context provider that loads application-specific objects into the workflow. +/// These providers can then be configured on a given workflow. +/// +public interface IWorkflowContextProvider +{ + /// + /// Implement this method to load an object into memory that is accessible throughout the lifetime of the workflow's current execution. + /// + ValueTask LoadAsync(WorkflowExecutionContext workflowExecutionContext); + + /// + /// Implement this method to save an object that was loaded previously + /// + ValueTask SaveAsync(WorkflowExecutionContext workflowExecutionContext, object? context); +} \ No newline at end of file diff --git a/src/modules/Elsa.Modules.WorkflowContexts/Elsa.Modules.WorkflowContexts.csproj b/src/modules/Elsa.Modules.WorkflowContexts/Elsa.Modules.WorkflowContexts.csproj new file mode 100644 index 000000000..d0cc0dd78 --- /dev/null +++ b/src/modules/Elsa.Modules.WorkflowContexts/Elsa.Modules.WorkflowContexts.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/src/modules/Elsa.Modules.WorkflowContexts/Extensions/WorkflowExecutionBuilderExtensions.cs b/src/modules/Elsa.Modules.WorkflowContexts/Extensions/WorkflowExecutionBuilderExtensions.cs new file mode 100644 index 000000000..1b7e00ad8 --- /dev/null +++ b/src/modules/Elsa.Modules.WorkflowContexts/Extensions/WorkflowExecutionBuilderExtensions.cs @@ -0,0 +1,13 @@ +using Elsa.Contracts; +using Elsa.Modules.WorkflowContexts.Middleware; +using Elsa.Pipelines.WorkflowExecution; + +namespace Elsa.Modules.WorkflowContexts.Extensions; + +public static class WorkflowExecutionBuilderExtensions +{ + /// + /// Installs the . + /// + public static IWorkflowExecutionBuilder UseWorkflowContexts(this IWorkflowExecutionBuilder builder) => builder.UseMiddleware(); +} \ No newline at end of file diff --git a/src/modules/Elsa.Modules.WorkflowContexts/Extensions/WorkflowExtensions.cs b/src/modules/Elsa.Modules.WorkflowContexts/Extensions/WorkflowExtensions.cs new file mode 100644 index 000000000..144208a23 --- /dev/null +++ b/src/modules/Elsa.Modules.WorkflowContexts/Extensions/WorkflowExtensions.cs @@ -0,0 +1,22 @@ +using Elsa.Contracts; +using Elsa.Extensions; +using Elsa.Modules.WorkflowContexts.Contracts; +using Elsa.Modules.WorkflowContexts.Models; + +namespace Elsa.Modules.WorkflowContexts.Extensions; + +public static class WorkflowExtensions +{ + /// + /// Installs the specified workflow context provider type into the specified workflow. + /// + public static IWorkflowDefinitionBuilder AddWorkflowContext(this IWorkflowDefinitionBuilder workflow, WorkflowContext workflowContext) where TProvider : IWorkflowContextProvider + { + var providerTypes = workflow.ApplicationProperties!.GetOrAdd("Elsa:WorkflowContexts", () => new List()); + + providerTypes.Add(workflowContext); + return workflow; + } + + +} \ No newline at end of file diff --git a/src/modules/Elsa.Modules.WorkflowContexts/Middleware/WorkflowContextMiddleware.cs b/src/modules/Elsa.Modules.WorkflowContexts/Middleware/WorkflowContextMiddleware.cs new file mode 100644 index 000000000..04e05b926 --- /dev/null +++ b/src/modules/Elsa.Modules.WorkflowContexts/Middleware/WorkflowContextMiddleware.cs @@ -0,0 +1,57 @@ +using Elsa.Extensions; +using Elsa.Models; +using Elsa.Modules.WorkflowContexts.Contracts; +using Elsa.Modules.WorkflowContexts.Models; +using Elsa.Pipelines.WorkflowExecution; +using Elsa.Pipelines.WorkflowExecution.Components; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Modules.WorkflowContexts.Middleware; + +/// +/// Middleware that loads & save workflow context into the currently executing workflow using installed workflow context providers. +/// +public class WorkflowContextMiddleware : WorkflowExecutionMiddleware +{ + private readonly IServiceProvider _serviceProvider; + + public WorkflowContextMiddleware(WorkflowMiddlewareDelegate next, IServiceProvider serviceProvider) : base(next) + { + _serviceProvider = serviceProvider; + } + + public override async ValueTask InvokeAsync(WorkflowExecutionContext context) + { + // Check if the workflow contains any workflow contexts. + if (!context.Workflow.ApplicationProperties!.TryGetValue>("Elsa:WorkflowContexts", out var workflowContexts)) + { + await Next(context); + return; + } + + // For each workflow context, invoke its provider. + foreach (var workflowContext in workflowContexts!) + { + var provider = (IWorkflowContextProvider)ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, workflowContext.ProviderType); + var value = await provider.LoadAsync(context); + + // Store the loaded value into the workflow execution context. + var contextDictionary = context.TransientProperties.GetOrAdd("WorkflowContexts", () => new Dictionary()); + contextDictionary.Add(workflowContext, value); + } + + // Invoke the next middleware. + await Next(context); + + // For each workflow context, invoke its provider to update the context. + foreach (var workflowContext in workflowContexts!) + { + // Get the loaded value from the workflow execution context. + var contextDictionary = context.TransientProperties.GetOrAdd("WorkflowContexts", () => new Dictionary()); + var value = contextDictionary[workflowContext]; + + var provider = (IWorkflowContextProvider)ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, workflowContext.ProviderType); + await provider.SaveAsync(context, value); + } + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Modules.WorkflowContexts/Models/WorkflowContext.cs b/src/modules/Elsa.Modules.WorkflowContexts/Models/WorkflowContext.cs new file mode 100644 index 000000000..2687f9e6f --- /dev/null +++ b/src/modules/Elsa.Modules.WorkflowContexts/Models/WorkflowContext.cs @@ -0,0 +1,28 @@ +using Elsa.Models; +using Elsa.Modules.WorkflowContexts.Contracts; + +namespace Elsa.Modules.WorkflowContexts.Models; + +public class WorkflowContext +{ + public WorkflowContext(Type providerType) + { + ProviderType = providerType; + } + + public Type ProviderType { get; } +} + +public class WorkflowContext : WorkflowContext where TProvider:IWorkflowContextProvider +{ + public WorkflowContext() : base(typeof(TProvider)) + { + } + + public T? Get(ExpressionExecutionContext context) + { + var workflowContexts = (IDictionary)context.TransientProperties["WorkflowContexts"]!; + return (T?)workflowContexts[this]; + + } +} \ No newline at end of file diff --git a/src/persistence/Elsa.Persistence.Abstractions/Entities/WorkflowDefinition.cs b/src/persistence/Elsa.Persistence.Abstractions/Entities/WorkflowDefinition.cs index 37dac738d..301b7927b 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Entities/WorkflowDefinition.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Entities/WorkflowDefinition.cs @@ -15,6 +15,7 @@ public class WorkflowDefinition : Entity public int Version { get; set; } = 1; public IActivity Root { get; set; } = default!; public ICollection Variables { get; set; } = new List(); + public IDictionary ApplicationProperties { get; set; } = new Dictionary(); public bool IsLatest { get; set; } public bool IsPublished { get; set; } diff --git a/src/persistence/Elsa.Persistence.Abstractions/Mappers/WorkflowDefinitionMapper.cs b/src/persistence/Elsa.Persistence.Abstractions/Mappers/WorkflowDefinitionMapper.cs index c41a8f610..2ef47b447 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Mappers/WorkflowDefinitionMapper.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Mappers/WorkflowDefinitionMapper.cs @@ -15,7 +15,8 @@ public class WorkflowDefinitionMapper new WorkflowPublication(definition.IsLatest, definition.IsPublished), new WorkflowMetadata(definition.Name, definition.Description, definition.CreatedAt), definition.Root, - definition.Variables); + definition.Variables, + definition.ApplicationProperties); } public WorkflowDefinition? Map(Workflow? workflow) => workflow == null ? null : Map(workflow, new WorkflowDefinition()); @@ -35,6 +36,8 @@ public class WorkflowDefinitionMapper definition.CreatedAt = metadata.CreatedAt; definition.IsLatest = publication.IsLatest; definition.IsPublished = publication.IsPublished; + definition.Variables = workflow.Variables; + definition.ApplicationProperties = workflow.ApplicationProperties; return definition; } diff --git a/src/persistence/Elsa.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220307110612_Initial.Designer.cs b/src/persistence/Elsa.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220308133708_Initial.Designer.cs similarity index 87% rename from src/persistence/Elsa.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220307110612_Initial.Designer.cs rename to src/persistence/Elsa.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220308133708_Initial.Designer.cs index c0e9c0958..37132c170 100644 --- a/src/persistence/Elsa.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220307110612_Initial.Designer.cs +++ b/src/persistence/Elsa.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220308133708_Initial.Designer.cs @@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Elsa.Persistence.EntityFrameworkCore.Sqlite.Migrations { [DbContext(typeof(ElsaDbContext))] - [Migration("20220307110612_Initial")] + [Migration("20220308133708_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -19,24 +19,6 @@ namespace Elsa.Persistence.EntityFrameworkCore.Sqlite.Migrations #pragma warning disable 612, 618 modelBuilder.HasAnnotation("ProductVersion", "6.0.1"); - modelBuilder.Entity("Elsa.Models.Variable", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("WorkflowDefinitionId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("WorkflowDefinitionId"); - - b.ToTable("Variable"); - }); - modelBuilder.Entity("Elsa.Persistence.Entities.WorkflowBookmark", b => { b.Property("Id") @@ -103,7 +85,7 @@ namespace Elsa.Persistence.EntityFrameworkCore.Sqlite.Migrations b.Property("Id") .HasColumnType("TEXT"); - b.Property("CreatedAt") + b.Property("CreatedAt") .HasColumnType("TEXT"); b.Property("Data") @@ -174,7 +156,7 @@ namespace Elsa.Persistence.EntityFrameworkCore.Sqlite.Migrations b.Property("Source") .HasColumnType("TEXT"); - b.Property("Timestamp") + b.Property("Timestamp") .HasColumnType("TEXT"); b.Property("WorkflowInstanceId") @@ -206,14 +188,14 @@ namespace Elsa.Persistence.EntityFrameworkCore.Sqlite.Migrations b.Property("Id") .HasColumnType("TEXT"); - b.Property("CancelledAt") + b.Property("CancelledAt") .HasColumnType("TEXT"); b.Property("CorrelationId") .IsRequired() .HasColumnType("TEXT"); - b.Property("CreatedAt") + b.Property("CreatedAt") .HasColumnType("TEXT"); b.Property("Data") @@ -227,13 +209,13 @@ namespace Elsa.Persistence.EntityFrameworkCore.Sqlite.Migrations .IsRequired() .HasColumnType("TEXT"); - b.Property("FaultedAt") + b.Property("FaultedAt") .HasColumnType("TEXT"); - b.Property("FinishedAt") + b.Property("FinishedAt") .HasColumnType("TEXT"); - b.Property("LastExecutedAt") + b.Property("LastExecutedAt") .HasColumnType("TEXT"); b.Property("Name") @@ -312,18 +294,6 @@ namespace Elsa.Persistence.EntityFrameworkCore.Sqlite.Migrations b.ToTable("WorkflowTriggers"); }); - - modelBuilder.Entity("Elsa.Models.Variable", b => - { - b.HasOne("Elsa.Persistence.Entities.WorkflowDefinition", null) - .WithMany("Variables") - .HasForeignKey("WorkflowDefinitionId"); - }); - - modelBuilder.Entity("Elsa.Persistence.Entities.WorkflowDefinition", b => - { - b.Navigation("Variables"); - }); #pragma warning restore 612, 618 } } diff --git a/src/persistence/Elsa.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220307110612_Initial.cs b/src/persistence/Elsa.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220308133708_Initial.cs similarity index 86% rename from src/persistence/Elsa.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220307110612_Initial.cs rename to src/persistence/Elsa.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220308133708_Initial.cs index 37e6609ce..b60bc9eb2 100644 --- a/src/persistence/Elsa.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220307110612_Initial.cs +++ b/src/persistence/Elsa.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220308133708_Initial.cs @@ -37,7 +37,7 @@ namespace Elsa.Persistence.EntityFrameworkCore.Sqlite.Migrations DefinitionId = table.Column(type: "TEXT", nullable: false), Name = table.Column(type: "TEXT", nullable: true), Description = table.Column(type: "TEXT", nullable: true), - CreatedAt = table.Column(type: "TEXT", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), Version = table.Column(type: "INTEGER", nullable: false), IsLatest = table.Column(type: "INTEGER", nullable: false), IsPublished = table.Column(type: "INTEGER", nullable: false), @@ -56,7 +56,7 @@ namespace Elsa.Persistence.EntityFrameworkCore.Sqlite.Migrations WorkflowInstanceId = table.Column(type: "TEXT", nullable: false), ActivityId = table.Column(type: "TEXT", nullable: false), ActivityType = table.Column(type: "TEXT", nullable: false), - Timestamp = table.Column(type: "TEXT", nullable: false), + Timestamp = table.Column(type: "TEXT", nullable: false), EventName = table.Column(type: "TEXT", nullable: true), Message = table.Column(type: "TEXT", nullable: true), Source = table.Column(type: "TEXT", nullable: true), @@ -78,11 +78,11 @@ namespace Elsa.Persistence.EntityFrameworkCore.Sqlite.Migrations WorkflowStatus = table.Column(type: "INTEGER", nullable: false), CorrelationId = table.Column(type: "TEXT", nullable: false), Name = table.Column(type: "TEXT", nullable: true), - CreatedAt = table.Column(type: "TEXT", nullable: false), - LastExecutedAt = table.Column(type: "TEXT", nullable: true), - FinishedAt = table.Column(type: "TEXT", nullable: true), - CancelledAt = table.Column(type: "TEXT", nullable: true), - FaultedAt = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + LastExecutedAt = table.Column(type: "TEXT", nullable: true), + FinishedAt = table.Column(type: "TEXT", nullable: true), + CancelledAt = table.Column(type: "TEXT", nullable: true), + FaultedAt = table.Column(type: "TEXT", nullable: true), Data = table.Column(type: "TEXT", nullable: true) }, constraints: table => @@ -105,29 +105,6 @@ namespace Elsa.Persistence.EntityFrameworkCore.Sqlite.Migrations table.PrimaryKey("PK_WorkflowTriggers", x => x.Id); }); - migrationBuilder.CreateTable( - name: "Variable", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - Name = table.Column(type: "TEXT", nullable: true), - WorkflowDefinitionId = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Variable", x => x.Id); - table.ForeignKey( - name: "FK_Variable_WorkflowDefinitions_WorkflowDefinitionId", - column: x => x.WorkflowDefinitionId, - principalTable: "WorkflowDefinitions", - principalColumn: "Id"); - }); - - migrationBuilder.CreateIndex( - name: "IX_Variable_WorkflowDefinitionId", - table: "Variable", - column: "WorkflowDefinitionId"); - migrationBuilder.CreateIndex( name: "IX_WorkflowBookmark_ActivityId", table: "WorkflowBookmarks", @@ -278,10 +255,10 @@ namespace Elsa.Persistence.EntityFrameworkCore.Sqlite.Migrations protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( - name: "Variable"); + name: "WorkflowBookmarks"); migrationBuilder.DropTable( - name: "WorkflowBookmarks"); + name: "WorkflowDefinitions"); migrationBuilder.DropTable( name: "WorkflowExecutionLogRecords"); @@ -291,9 +268,6 @@ namespace Elsa.Persistence.EntityFrameworkCore.Sqlite.Migrations migrationBuilder.DropTable( name: "WorkflowTriggers"); - - migrationBuilder.DropTable( - name: "WorkflowDefinitions"); } } } diff --git a/src/persistence/Elsa.Persistence.EntityFrameworkCore.Sqlite/Migrations/ElsaDbContextModelSnapshot.cs b/src/persistence/Elsa.Persistence.EntityFrameworkCore.Sqlite/Migrations/ElsaDbContextModelSnapshot.cs index 425fd4931..d505cec79 100644 --- a/src/persistence/Elsa.Persistence.EntityFrameworkCore.Sqlite/Migrations/ElsaDbContextModelSnapshot.cs +++ b/src/persistence/Elsa.Persistence.EntityFrameworkCore.Sqlite/Migrations/ElsaDbContextModelSnapshot.cs @@ -17,24 +17,6 @@ namespace Elsa.Persistence.EntityFrameworkCore.Sqlite.Migrations #pragma warning disable 612, 618 modelBuilder.HasAnnotation("ProductVersion", "6.0.1"); - modelBuilder.Entity("Elsa.Models.Variable", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("WorkflowDefinitionId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("WorkflowDefinitionId"); - - b.ToTable("Variable"); - }); - modelBuilder.Entity("Elsa.Persistence.Entities.WorkflowBookmark", b => { b.Property("Id") @@ -101,7 +83,7 @@ namespace Elsa.Persistence.EntityFrameworkCore.Sqlite.Migrations b.Property("Id") .HasColumnType("TEXT"); - b.Property("CreatedAt") + b.Property("CreatedAt") .HasColumnType("TEXT"); b.Property("Data") @@ -172,7 +154,7 @@ namespace Elsa.Persistence.EntityFrameworkCore.Sqlite.Migrations b.Property("Source") .HasColumnType("TEXT"); - b.Property("Timestamp") + b.Property("Timestamp") .HasColumnType("TEXT"); b.Property("WorkflowInstanceId") @@ -204,14 +186,14 @@ namespace Elsa.Persistence.EntityFrameworkCore.Sqlite.Migrations b.Property("Id") .HasColumnType("TEXT"); - b.Property("CancelledAt") + b.Property("CancelledAt") .HasColumnType("TEXT"); b.Property("CorrelationId") .IsRequired() .HasColumnType("TEXT"); - b.Property("CreatedAt") + b.Property("CreatedAt") .HasColumnType("TEXT"); b.Property("Data") @@ -225,13 +207,13 @@ namespace Elsa.Persistence.EntityFrameworkCore.Sqlite.Migrations .IsRequired() .HasColumnType("TEXT"); - b.Property("FaultedAt") + b.Property("FaultedAt") .HasColumnType("TEXT"); - b.Property("FinishedAt") + b.Property("FinishedAt") .HasColumnType("TEXT"); - b.Property("LastExecutedAt") + b.Property("LastExecutedAt") .HasColumnType("TEXT"); b.Property("Name") @@ -310,18 +292,6 @@ namespace Elsa.Persistence.EntityFrameworkCore.Sqlite.Migrations b.ToTable("WorkflowTriggers"); }); - - modelBuilder.Entity("Elsa.Models.Variable", b => - { - b.HasOne("Elsa.Persistence.Entities.WorkflowDefinition", null) - .WithMany("Variables") - .HasForeignKey("WorkflowDefinitionId"); - }); - - modelBuilder.Entity("Elsa.Persistence.Entities.WorkflowDefinition", b => - { - b.Navigation("Variables"); - }); #pragma warning restore 612, 618 } } diff --git a/src/persistence/Elsa.Persistence.EntityFrameworkCore/Configuration/WorkflowDefinitionConfiguration.cs b/src/persistence/Elsa.Persistence.EntityFrameworkCore/Configuration/WorkflowDefinitionConfiguration.cs index 2c0f12af3..b925fe0f4 100644 --- a/src/persistence/Elsa.Persistence.EntityFrameworkCore/Configuration/WorkflowDefinitionConfiguration.cs +++ b/src/persistence/Elsa.Persistence.EntityFrameworkCore/Configuration/WorkflowDefinitionConfiguration.cs @@ -9,6 +9,8 @@ namespace Elsa.Persistence.EntityFrameworkCore.Configuration public void Configure(EntityTypeBuilder builder) { builder.Ignore(x => x.Root); + builder.Ignore(x => x.Variables); + builder.Ignore(x => x.ApplicationProperties); builder.Property("Data"); builder.HasIndex(x => new {x.DefinitionId, x.Version}).HasDatabaseName($"IX_{nameof(WorkflowDefinition)}_{nameof(WorkflowDefinition.DefinitionId)}_{nameof(WorkflowDefinition.Version)}").IsUnique(); diff --git a/src/persistence/Elsa.Persistence.EntityFrameworkCore/Handlers/Serialization/WorkflowDefinitionSerializer.cs b/src/persistence/Elsa.Persistence.EntityFrameworkCore/Handlers/Serialization/WorkflowDefinitionSerializer.cs index 286d1419d..b3c6c5d71 100644 --- a/src/persistence/Elsa.Persistence.EntityFrameworkCore/Handlers/Serialization/WorkflowDefinitionSerializer.cs +++ b/src/persistence/Elsa.Persistence.EntityFrameworkCore/Handlers/Serialization/WorkflowDefinitionSerializer.cs @@ -1,6 +1,7 @@ using System.Text.Json; using Elsa.Contracts; using Elsa.Management.Serialization; +using Elsa.Models; using Elsa.Persistence.Entities; using Elsa.Persistence.EntityFrameworkCore.Contracts; @@ -20,6 +21,8 @@ public class WorkflowDefinitionSerializer : IEntitySerializer variables, IDictionary applicationProperties) { Root = root; + Variables = variables; + ApplicationProperties = applicationProperties; } public IActivity Root { get; init; } = default!; + public ICollection Variables { get; set; } = new List(); + public IDictionary ApplicationProperties { get; set; } = new Dictionary(); } } \ No newline at end of file diff --git a/src/runtime/Elsa.Runtime/Services/TriggerIndexer.cs b/src/runtime/Elsa.Runtime/Services/TriggerIndexer.cs index 85942aca9..c013fedbb 100644 --- a/src/runtime/Elsa.Runtime/Services/TriggerIndexer.cs +++ b/src/runtime/Elsa.Runtime/Services/TriggerIndexer.cs @@ -114,7 +114,7 @@ public class TriggerIndexer : ITriggerIndexer private async IAsyncEnumerable GetTriggersAsync(Workflow workflow, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var context = new WorkflowIndexingContext(workflow, cancellationToken); - + // Get a list of activities that are configured as "startable". var startableNodes = _activityWalker .Walk(workflow.Root) @@ -173,7 +173,7 @@ public class TriggerIndexer : ITriggerIndexer Hash = _hasher.Hash(x), Data = JsonSerializer.Serialize(x) }); - + return triggers.ToList(); } @@ -183,7 +183,7 @@ public class TriggerIndexer : ITriggerIndexer var assignedInputs = inputs.Where(x => x.LocationReference != null!).ToList(); var register = context.GetOrCreateRegister(trigger); var cancellationToken = context.CancellationToken; - var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, register, default, cancellationToken); + var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, register, context.Workflow, new Dictionary(), default, cancellationToken); // Evaluate activity inputs before requesting trigger data. foreach (var input in assignedInputs) diff --git a/src/samples/aspnet/Elsa.Samples.Web1/Elsa.Samples.Web1.csproj b/src/samples/aspnet/Elsa.Samples.Web1/Elsa.Samples.Web1.csproj index d3338bb94..c705dd3a4 100644 --- a/src/samples/aspnet/Elsa.Samples.Web1/Elsa.Samples.Web1.csproj +++ b/src/samples/aspnet/Elsa.Samples.Web1/Elsa.Samples.Web1.csproj @@ -14,6 +14,7 @@ + diff --git a/src/samples/aspnet/Elsa.Samples.Web1/Program.cs b/src/samples/aspnet/Elsa.Samples.Web1/Program.cs index 39035cd65..1601e0489 100644 --- a/src/samples/aspnet/Elsa.Samples.Web1/Program.cs +++ b/src/samples/aspnet/Elsa.Samples.Web1/Program.cs @@ -17,6 +17,7 @@ using Elsa.Modules.Quartz.Services; using Elsa.Modules.Scheduling.Activities; using Elsa.Modules.Scheduling.Extensions; using Elsa.Modules.Scheduling.Triggers; +using Elsa.Modules.WorkflowContexts.Extensions; using Elsa.Persistence.EntityFrameworkCore.Extensions; using Elsa.Persistence.EntityFrameworkCore.Sqlite; using Elsa.Pipelines.WorkflowExecution.Components; @@ -58,6 +59,7 @@ services options.Workflows.Add(nameof(SendMessageWorkflow), new SendMessageWorkflow()); options.Workflows.Add(nameof(ReceiveMessageWorkflow), new ReceiveMessageWorkflow()); options.Workflows.Add(nameof(RunJavaScriptWorkflow), new RunJavaScriptWorkflow()); + options.Workflows.Add(nameof(WorkflowContextsWorkflow), new WorkflowContextsWorkflow()); }); // Testing only: allow client app to connect from anywhere. @@ -101,6 +103,7 @@ serviceProvider.ConfigureDefaultWorkflowExecutionPipeline(pipeline => .UseWorkflowExecutionEvents() .UseWorkflowExecutionLogPersistence() .UsePersistence() + .UseWorkflowContexts() .UseActivityScheduler() ); diff --git a/src/samples/aspnet/Elsa.Samples.Web1/Workflows/WorkflowContextsWorkflow.cs b/src/samples/aspnet/Elsa.Samples.Web1/Workflows/WorkflowContextsWorkflow.cs new file mode 100644 index 000000000..02331a364 --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.Web1/Workflows/WorkflowContextsWorkflow.cs @@ -0,0 +1,43 @@ +using Elsa.Activities.Console; +using Elsa.Contracts; +using Elsa.Models; +using Elsa.Modules.WorkflowContexts.Abstractions; +using Elsa.Modules.WorkflowContexts.Extensions; +using Elsa.Modules.WorkflowContexts.Models; +using Elsa.Runtime.Contracts; + +namespace Elsa.Samples.Web1.Workflows; + +public class WorkflowContextsWorkflow : IWorkflow +{ + public void Build(IWorkflowDefinitionBuilder workflow) + { + var documentContext = new WorkflowContext(); + + workflow + .AddWorkflowContext(documentContext) + .WithRoot(new WriteLine(context => $"Document title: {documentContext.Get(context)!.Title}")); + } +} + +public class DocumentProvider : WorkflowContextProvider +{ + protected override Document? Load(WorkflowExecutionContext workflowExecutionContext) + { + var idGenerator = workflowExecutionContext.GetRequiredService(); + + return new Document + { + Id = idGenerator.GenerateId(), + Title = "Requirements", + Body = "Workflows should be able to load contextual data easily" + }; + } +} + +public class Document +{ + public string Id { get; set; } = default!; + public string Title { get; set; } = default!; + public string Body { get; set; } = default!; +} \ No newline at end of file