From 558486920bfcea3550bc9abe2919f677d0e3eb63 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 10 Jul 2024 10:29:45 +0200 Subject: [PATCH] Add PropertyBag for non-polymorphic data transfer between server and client (#5735) * Fix issues with workflow deletion, newline, and DB context. Retract published workflows before deletion to avoid exceptions. Added missing newline at EOF for consistency. Removed redundant DisposeAsync call in migration service. * Add PropertyBag for storing custom workflow properties Replaced `CustomProperties` with `PropertyBag` class for more structured property management across workflows. Adjusted serializations, extensions, and middleware for seamless integration with the new `PropertyBag` structure. --- .../Extensions/PropertyBagExtensions.cs | 41 ++++++++++++ .../Models/WorkflowDefinition.cs | 5 ++ .../Models/WorkflowDefinitionModel.cs | 5 ++ .../Shared/Models/PropertyBag.cs | 20 ++++++ .../Shared/Models/WrappedInput.cs | 3 +- .../Extensions/PropertyBagExtensions.cs | 67 +++++++++++++++++++ src/modules/Elsa.Common/Models/PropertyBag.cs | 20 ++++++ .../Elsa.Common/Models/VersionOptions.cs | 2 +- .../RunMigrationsHostedService.cs | 1 - .../Modules/Management/Configurations.cs | 1 + .../Management/WorkflowDefinitionStore.cs | 13 +++- ...lowContextWorkflowDefinitionExtensions.cs} | 10 +-- ...kflowContextActivityExecutionMiddleware.cs | 41 ++++++------ .../StaticWorkflowDefinitionLinker.cs | 2 + .../Activities/Workflow.cs | 8 +++ .../Builders/WorkflowBuilder.cs | 38 ++++------- .../Contracts/IWorkflowBuilder.cs | 7 ++ .../Entities/WorkflowDefinition.cs | 7 +- .../Mappers/WorkflowDefinitionMapper.cs | 4 ++ .../Models/WorkflowDefinitionModel.cs | 8 ++- .../Services/WorkflowDefinitionManager.cs | 2 +- 21 files changed, 242 insertions(+), 63 deletions(-) create mode 100644 src/clients/Elsa.Api.Client/Extensions/PropertyBagExtensions.cs create mode 100644 src/clients/Elsa.Api.Client/Shared/Models/PropertyBag.cs create mode 100644 src/modules/Elsa.Common/Extensions/PropertyBagExtensions.cs create mode 100644 src/modules/Elsa.Common/Models/PropertyBag.cs rename src/modules/Elsa.WorkflowContexts/Extensions/{WorkflowDefinitionExtensions.cs => WorkflowContextWorkflowDefinitionExtensions.cs} (69%) diff --git a/src/clients/Elsa.Api.Client/Extensions/PropertyBagExtensions.cs b/src/clients/Elsa.Api.Client/Extensions/PropertyBagExtensions.cs new file mode 100644 index 000000000..c27200c88 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Extensions/PropertyBagExtensions.cs @@ -0,0 +1,41 @@ +using System.Text.Json; +using Elsa.Api.Client.Shared.Models; + +namespace Elsa.Api.Client.Extensions; + +/// +/// Provides extension methods for the PropertyBag class. +/// +public static class PropertyBagExtensions +{ + /// + /// Tries to retrieve a value from the PropertyBag based on the provided key. + /// If the specified key does not exist in the PropertyBag, the method will return the default value obtained from the defaultValue function. + /// + /// The type of the value to retrieve. + /// The PropertyBag to retrieve the value from. + /// The key of the value to retrieve. + /// A function that returns the default value to be returned if the key does not exist in the PropertyBag. + /// The value associated with the key, or the default value if the key does not exist. + public static T TryGetValueOrDefault(this PropertyBag propertyBag, string key, Func defaultValue) + { + if (!propertyBag.TryGetValue(key, out var value)) + return defaultValue(); + + var json = (string)value; + return JsonSerializer.Deserialize(json); + } + + /// + /// Sets a value in the PropertyBag based on the provided key. + /// The value is serialized using JSON. + /// + /// The PropertyBag to set the value in. + /// The key to associate with the value. + /// The value to store in the PropertyBag. + public static void SetValue(this PropertyBag propertyBag, string key, object value) + { + var json = JsonSerializer.Serialize(value); + propertyBag[key] = json; + } +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinition.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinition.cs index f3fe5d0eb..4d2ba3ef5 100644 --- a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinition.cs +++ b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinition.cs @@ -58,6 +58,11 @@ public class WorkflowDefinition : LinkedEntity /// public IDictionary CustomProperties { get; set; } = new Dictionary(); + /// + /// Stores custom information about the workflow. Can be used to store application-specific properties to associate with the workflow. + /// + public PropertyBag PropertyBag { get; set; } = new(); + /// /// The name of the workflow provider that created this workflow, if any. /// diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinitionModel.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinitionModel.cs index a617b54f3..76dba636d 100644 --- a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinitionModel.cs +++ b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinitionModel.cs @@ -55,6 +55,11 @@ public class WorkflowDefinitionModel : LinkedEntity /// public IDictionary? CustomProperties { get; set; } + /// + /// Stores custom information about the workflow. Can be used to store application-specific properties to associate with the workflow. + /// + public PropertyBag PropertyBag { get; set; } = new(); + /// /// Gets or sets whether the workflow definition is read-only. /// diff --git a/src/clients/Elsa.Api.Client/Shared/Models/PropertyBag.cs b/src/clients/Elsa.Api.Client/Shared/Models/PropertyBag.cs new file mode 100644 index 000000000..9abe27abc --- /dev/null +++ b/src/clients/Elsa.Api.Client/Shared/Models/PropertyBag.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace Elsa.Api.Client.Shared.Models; + +/// A dictionary of values that is skipped by polymorphic serialization. +public class PropertyBag : Dictionary +{ + /// + [JsonConstructor] + public PropertyBag() : base(StringComparer.OrdinalIgnoreCase) + { + } + + /// + public PropertyBag(IDictionary dictionary) : this() + { + foreach (var kvp in dictionary) + Add(kvp.Key, kvp.Value); + } +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Shared/Models/WrappedInput.cs b/src/clients/Elsa.Api.Client/Shared/Models/WrappedInput.cs index ea5c9225e..072f437fe 100644 --- a/src/clients/Elsa.Api.Client/Shared/Models/WrappedInput.cs +++ b/src/clients/Elsa.Api.Client/Shared/Models/WrappedInput.cs @@ -21,4 +21,5 @@ public class WrappedInput /// Gets or sets the memory reference of this input. /// public MemoryReference MemoryReference { get; set; } = default!; -} \ No newline at end of file +} + diff --git a/src/modules/Elsa.Common/Extensions/PropertyBagExtensions.cs b/src/modules/Elsa.Common/Extensions/PropertyBagExtensions.cs new file mode 100644 index 000000000..ad0359c21 --- /dev/null +++ b/src/modules/Elsa.Common/Extensions/PropertyBagExtensions.cs @@ -0,0 +1,67 @@ +using System.Text.Json; +using Elsa.Common.Models; + +// ReSharper disable once CheckNamespace +namespace Elsa.Extensions; + +/// +/// Provides extension methods for the PropertyBag class. +/// +public static class PropertyBagExtensions +{ + /// + /// Tries to retrieve a value from the PropertyBag based on the provided key. + /// If the specified key does not exist in the PropertyBag, the method will return the default value obtained from the defaultValue function. + /// + /// The type of the value to retrieve. + /// The PropertyBag to retrieve the value from. + /// The key of the value to retrieve. + /// A function that returns the default value to be returned if the key does not exist in the PropertyBag. + /// Optional JSON serializer options. + /// The value associated with the key, or the default value if the key does not exist. + public static T TryGetValueOrDefault(this PropertyBag propertyBag, string key, Func defaultValue, JsonSerializerOptions? options = null) + { + if (!propertyBag.TryGetValue(key, out var value)) + return defaultValue(); + + var json = (string)value; + return JsonSerializer.Deserialize(json, options); + } + + /// + /// Tries to retrieve a value from the PropertyBag based on the provided key. + /// If the specified key does not exist in the PropertyBag, the method will return the default value obtained from the defaultValue function. + /// + /// The type of the value to retrieve. + /// The PropertyBag to retrieve the value from. + /// The key of the value to retrieve. + /// The deserialized value. + /// Optional JSON serializer options. + /// True if the value exists, false otherwise. + public static bool TryGetValue(this PropertyBag propertyBag, string key, out T value, JsonSerializerOptions? options = null) + { + if (!propertyBag.TryGetValue(key, out var v)) + { + value = default!; + return false; + } + + var json = (string)v; + value = JsonSerializer.Deserialize(json, options); + return true; + } + + /// + /// Sets a value in the PropertyBag based on the provided key. + /// The value is serialized using JSON. + /// + /// The PropertyBag to set the value in. + /// The key to associate with the value. + /// The value to store in the PropertyBag. + /// /// Optional JSON serializer options. + public static void SetValue(this PropertyBag propertyBag, string key, object value, JsonSerializerOptions? options = null) + { + var json = JsonSerializer.Serialize(value); + propertyBag[key] = json; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Common/Models/PropertyBag.cs b/src/modules/Elsa.Common/Models/PropertyBag.cs new file mode 100644 index 000000000..0f6209e81 --- /dev/null +++ b/src/modules/Elsa.Common/Models/PropertyBag.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace Elsa.Common.Models; + +/// A dictionary of values that is skipped by polymorphic serialization. +public class PropertyBag : Dictionary +{ + /// + [JsonConstructor] + public PropertyBag() : base(StringComparer.OrdinalIgnoreCase) + { + } + + /// + public PropertyBag(IDictionary dictionary) : this() + { + foreach (var kvp in dictionary) + Add(kvp.Key, kvp.Value); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Common/Models/VersionOptions.cs b/src/modules/Elsa.Common/Models/VersionOptions.cs index f3c56f598..40ab3a0ad 100644 --- a/src/modules/Elsa.Common/Models/VersionOptions.cs +++ b/src/modules/Elsa.Common/Models/VersionOptions.cs @@ -114,4 +114,4 @@ public struct VersionOptions /// Returns a simple string representation of this . /// public override string ToString() => AllVersions ? "AllVersions" : IsDraft ? "Draft" : IsLatest ? "Latest" : IsPublished ? "Published" : IsLatestOrPublished ? "LatestOrPublished" : IsLatestAndPublished ? "LatestAndPublished" : Version.ToString(); -} +} \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/RunMigrationsHostedService.cs b/src/modules/Elsa.EntityFrameworkCore.Common/RunMigrationsHostedService.cs index ebfb92beb..5d5f4e047 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/RunMigrationsHostedService.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/RunMigrationsHostedService.cs @@ -26,7 +26,6 @@ public class RunMigrationsHostedService : IHostedService where TDbCo var dbContextFactory = scope.ServiceProvider.GetRequiredService>(); await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); await dbContext.Database.MigrateAsync(cancellationToken); - await dbContext.DisposeAsync(); } /// diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/Configurations.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/Configurations.cs index 02cdccc5c..7f25567dd 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/Configurations.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/Configurations.cs @@ -17,6 +17,7 @@ internal class Configurations : IEntityTypeConfiguration, IE builder.Ignore(x => x.Outputs); builder.Ignore(x => x.Outcomes); builder.Ignore(x => x.CustomProperties); + builder.Ignore(x => x.PropertyBag); builder.Ignore(x => x.Options); builder.Property("Data"); builder.Property("UsableAsActivity"); diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionStore.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionStore.cs index 2c6886075..7421e5938 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionStore.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionStore.cs @@ -152,7 +152,7 @@ public class EFCoreWorkflowDefinitionStore : IWorkflowDefinitionStore private ValueTask OnSaveAsync(ManagementElsaDbContext managementElsaDbContext, WorkflowDefinition entity, CancellationToken cancellationToken) { - var data = new WorkflowDefinitionState(entity.Options, entity.Variables, entity.Inputs, entity.Outputs, entity.Outcomes, entity.CustomProperties); + var data = new WorkflowDefinitionState(entity.Options, entity.Variables, entity.Inputs, entity.Outputs, entity.Outcomes, entity.CustomProperties, entity.PropertyBag); var json = _payloadSerializer.Serialize(data); managementElsaDbContext.Entry(entity).Property("Data").CurrentValue = json; @@ -165,7 +165,7 @@ public class EFCoreWorkflowDefinitionStore : IWorkflowDefinitionStore if (entity == null) return ValueTask.CompletedTask; - var data = new WorkflowDefinitionState(entity.Options, entity.Variables, entity.Inputs, entity.Outputs, entity.Outcomes, entity.CustomProperties); + var data = new WorkflowDefinitionState(entity.Options, entity.Variables, entity.Inputs, entity.Outputs, entity.Outcomes, entity.CustomProperties, entity.PropertyBag); var json = (string?)managementElsaDbContext.Entry(entity).Property("Data").CurrentValue; if (!string.IsNullOrWhiteSpace(json)) @@ -177,6 +177,7 @@ public class EFCoreWorkflowDefinitionStore : IWorkflowDefinitionStore entity.Outputs = data.Outputs; entity.Outcomes = data.Outcomes; entity.CustomProperties = data.CustomProperties; + entity.PropertyBag = data.PropertyBag; return ValueTask.CompletedTask; } @@ -225,7 +226,8 @@ public class EFCoreWorkflowDefinitionStore : IWorkflowDefinitionStore ICollection inputs, ICollection outputs, ICollection outcomes, - IDictionary customProperties + IDictionary customProperties, + PropertyBag propertyBag ) { Options = options; @@ -234,6 +236,7 @@ public class EFCoreWorkflowDefinitionStore : IWorkflowDefinitionStore Outputs = outputs; Outcomes = outcomes; CustomProperties = customProperties; + PropertyBag = propertyBag; } public WorkflowOptions Options { get; set; } = new(); @@ -241,6 +244,10 @@ public class EFCoreWorkflowDefinitionStore : IWorkflowDefinitionStore public ICollection Inputs { get; set; } = new List(); public ICollection Outputs { get; set; } = new List(); public ICollection Outcomes { get; set; } = new List(); + + [Obsolete("Use PropertyBag instead")] public IDictionary CustomProperties { get; set; } = new Dictionary(); + + public PropertyBag PropertyBag { get; set; } = new(); } } \ No newline at end of file diff --git a/src/modules/Elsa.WorkflowContexts/Extensions/WorkflowDefinitionExtensions.cs b/src/modules/Elsa.WorkflowContexts/Extensions/WorkflowContextWorkflowDefinitionExtensions.cs similarity index 69% rename from src/modules/Elsa.WorkflowContexts/Extensions/WorkflowDefinitionExtensions.cs rename to src/modules/Elsa.WorkflowContexts/Extensions/WorkflowContextWorkflowDefinitionExtensions.cs index 1940aa46d..fc8279df0 100644 --- a/src/modules/Elsa.WorkflowContexts/Extensions/WorkflowDefinitionExtensions.cs +++ b/src/modules/Elsa.WorkflowContexts/Extensions/WorkflowContextWorkflowDefinitionExtensions.cs @@ -4,16 +4,16 @@ using Elsa.Workflows.Management.Entities; // ReSharper disable once CheckNamespace namespace Elsa.Extensions; -/// /// Adds extension methods to . -/// -public static class WorkflowDefinitionExtensions +public static class WorkflowContextWorkflowDefinitionExtensions { /// /// Gets the workflow context provider types that are installed on the workflow definition. /// /// The workflow definition to get the provider types from. /// The workflow context provider types. - public static IEnumerable GetWorkflowContextProviderTypes(this WorkflowDefinition workflowDefinition) => - workflowDefinition.CustomProperties.GetOrAdd(Constants.WorkflowContextProviderTypesKey, () => new List()); + public static IEnumerable GetWorkflowContextProviderTypes(this WorkflowDefinition workflowDefinition) + { + return workflowDefinition.PropertyBag.GetOrAdd(Constants.WorkflowContextProviderTypesKey, () => new List()); + } } \ No newline at end of file diff --git a/src/modules/Elsa.WorkflowContexts/Middleware/WorkflowContextActivityExecutionMiddleware.cs b/src/modules/Elsa.WorkflowContexts/Middleware/WorkflowContextActivityExecutionMiddleware.cs index ccb7d4ba2..7b1cda1bc 100644 --- a/src/modules/Elsa.WorkflowContexts/Middleware/WorkflowContextActivityExecutionMiddleware.cs +++ b/src/modules/Elsa.WorkflowContexts/Middleware/WorkflowContextActivityExecutionMiddleware.cs @@ -1,37 +1,38 @@ +using System.Text.Json; +using Elsa.Expressions.Contracts; using Elsa.Extensions; using Elsa.WorkflowContexts.Contracts; using Elsa.Workflows; using Elsa.Workflows.Contracts; using Elsa.Workflows.Pipelines.ActivityExecution; -using Elsa.Workflows.Runtime.Middleware.Activities; +using Elsa.Workflows.Serialization.Converters; +using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; namespace Elsa.WorkflowContexts.Middleware; -/// /// Middleware that loads and save workflow context into the currently executing workflow using installed workflow context providers. -/// -public class WorkflowContextActivityExecutionMiddleware : IActivityExecutionMiddleware +[UsedImplicitly] +public class WorkflowContextActivityExecutionMiddleware(ActivityMiddlewareDelegate next, IServiceScopeFactory serviceScopeFactory, IWellKnownTypeRegistry wellKnownTypeRegistry) : IActivityExecutionMiddleware { - private readonly ActivityMiddlewareDelegate _next; - private readonly IServiceScopeFactory _serviceScopeFactory; - - /// - /// Constructor. - /// - public WorkflowContextActivityExecutionMiddleware(ActivityMiddlewareDelegate next, IServiceScopeFactory serviceScopeFactory) + private readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions { - _next = next; - _serviceScopeFactory = serviceScopeFactory; - } + PropertyNameCaseInsensitive = true + }.WithConverters(new TypeJsonConverter(wellKnownTypeRegistry)); /// public async ValueTask InvokeAsync(ActivityExecutionContext context) { // Check if the workflow contains any workflow context providers. - if (!context.WorkflowExecutionContext.Workflow.CustomProperties.TryGetValue>(Constants.WorkflowContextProviderTypesKey, out var providerTypes)) + if (!context.WorkflowExecutionContext.Workflow.PropertyBag.TryGetValue>(Constants.WorkflowContextProviderTypesKey, out var providerTypes, _jsonSerializerOptions)) { - await _next(context); + await next(context); + return; + } + + if (providerTypes.Count == 0) + { + await next(context); return; } @@ -41,12 +42,12 @@ public class WorkflowContextActivityExecutionMiddleware : IActivityExecutionMidd // Is the activity configured to load the context? foreach (var providerType in providerTypes) { - // Is the activity configured to load the context or is this a background execution? + // Is the activity configured to load the context, or is this a background execution? var load = isBackgroundExecution || context.Activity.GetActivityWorkflowContextSettings(providerType).Load; if (!load) continue; // Load the context. - using var scope = _serviceScopeFactory.CreateScope(); + using var scope = serviceScopeFactory.CreateScope(); var provider = (IWorkflowContextProvider)ActivatorUtilities.GetServiceOrCreateInstance(scope.ServiceProvider, providerType); var value = await provider.LoadAsync(context.WorkflowExecutionContext); @@ -55,7 +56,7 @@ public class WorkflowContextActivityExecutionMiddleware : IActivityExecutionMidd } // Invoke the next middleware. - await _next(context); + await next(context); // Invoke each workflow context provider to persists the context. foreach (var providerType in providerTypes) @@ -65,7 +66,7 @@ public class WorkflowContextActivityExecutionMiddleware : IActivityExecutionMidd if (!save) continue; // Get the loaded value from the workflow execution context. - using var scope = _serviceScopeFactory.CreateScope(); + using var scope = serviceScopeFactory.CreateScope(); var value = context.WorkflowExecutionContext.GetWorkflowContext(providerType); // Save the context. diff --git a/src/modules/Elsa.Workflows.Api/Services/StaticWorkflowDefinitionLinker.cs b/src/modules/Elsa.Workflows.Api/Services/StaticWorkflowDefinitionLinker.cs index da2a3351e..d065b5818 100644 --- a/src/modules/Elsa.Workflows.Api/Services/StaticWorkflowDefinitionLinker.cs +++ b/src/modules/Elsa.Workflows.Api/Services/StaticWorkflowDefinitionLinker.cs @@ -32,6 +32,7 @@ public class StaticWorkflowDefinitionLinker( Outputs = workflowDefinitionModel.Outputs, Outcomes = workflowDefinitionModel.Outcomes, CustomProperties = workflowDefinitionModel.CustomProperties, + PropertyBag = workflowDefinitionModel.PropertyBag, IsReadonly = workflowDefinitionModel.IsReadonly, IsSystem = workflowDefinitionModel.IsSystem, IsLatest = workflowDefinitionModel.IsLatest, @@ -99,6 +100,7 @@ public class StaticWorkflowDefinitionLinker( Outputs = item.Outputs, Outcomes = item.Outcomes, CustomProperties = item.CustomProperties, + PropertyBag = item.PropertyBag, IsReadonly = item.IsReadonly, IsSystem = item.IsSystem, IsLatest = item.IsLatest, diff --git a/src/modules/Elsa.Workflows.Core/Activities/Workflow.cs b/src/modules/Elsa.Workflows.Core/Activities/Workflow.cs index 1116beb80..d366bd17d 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Workflow.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Workflow.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using Elsa.Common.Models; using Elsa.Expressions.Models; using Elsa.Workflows.Attributes; using Elsa.Workflows.Contracts; @@ -30,6 +31,7 @@ public class Workflow : Composite, ICloneable ICollection outputs, ICollection outcomes, IDictionary customProperties, + PropertyBag propertyBag, bool isReadonly, bool isSystem) { @@ -38,6 +40,7 @@ public class Workflow : Composite, ICloneable Inputs = inputs; Outputs = outputs; Outcomes = outcomes; + PropertyBag = propertyBag; WorkflowMetadata = workflowMetadata; Options = options; Variables = variables; @@ -96,6 +99,11 @@ public class Workflow : Composite, ICloneable /// Gets or sets options for the workflow. /// public WorkflowOptions Options { get; set; } = new(); + + /// + /// A bag of properties that can be used by applications and modules to store information that can be shared with tooling. + /// + public PropertyBag PropertyBag { get; set; } = new(); /// /// Make workflow definition readonly. diff --git a/src/modules/Elsa.Workflows.Core/Builders/WorkflowBuilder.cs b/src/modules/Elsa.Workflows.Core/Builders/WorkflowBuilder.cs index 72dd989bd..246ee7bad 100644 --- a/src/modules/Elsa.Workflows.Core/Builders/WorkflowBuilder.cs +++ b/src/modules/Elsa.Workflows.Core/Builders/WorkflowBuilder.cs @@ -1,3 +1,4 @@ +using Elsa.Common.Models; using Elsa.Extensions; using Elsa.Workflows.Activities; using Elsa.Workflows.Contracts; @@ -7,25 +8,9 @@ using Elsa.Workflows.Models; namespace Elsa.Workflows.Builders; /// -public class WorkflowBuilder : IWorkflowBuilder +public class WorkflowBuilder(IActivityVisitor activityVisitor, IIdentityGraphService identityGraphService, IActivityRegistry activityRegistry, IIdentityGenerator identityGenerator) + : IWorkflowBuilder { - private readonly IActivityVisitor _activityVisitor; - private readonly IIdentityGraphService _identityGraphService; - private readonly IActivityRegistry _activityRegistry; - private readonly IIdentityGenerator _identityGenerator; - - /// - /// Constructor. - /// - public WorkflowBuilder(IActivityVisitor activityVisitor, IIdentityGraphService identityGraphService, IActivityRegistry activityRegistry, IIdentityGenerator identityGenerator) - { - _activityVisitor = activityVisitor; - _identityGraphService = identityGraphService; - _activityRegistry = activityRegistry; - _identityGenerator = identityGenerator; - Result = new Variable(); - } - /// public string? Id { get; set; } @@ -63,11 +48,14 @@ public class WorkflowBuilder : IWorkflowBuilder public ICollection Outcomes { get; set; } = new List(); /// - public Variable? Result { get; set; } + public Variable? Result { get; set; } = new(); /// public IDictionary CustomProperties { get; set; } = new Dictionary(); + /// + public PropertyBag PropertyBag { get; set; } = new(); + /// public WorkflowOptions WorkflowOptions { get; } = new(); @@ -216,14 +204,14 @@ public class WorkflowBuilder : IWorkflowBuilder /// public async Task BuildWorkflowAsync(CancellationToken cancellationToken = default) { - var definitionId = DefinitionId ?? _identityGenerator.GenerateId(); - var id = Id ?? _identityGenerator.GenerateId(); + var definitionId = DefinitionId ?? identityGenerator.GenerateId(); + var id = Id ?? identityGenerator.GenerateId(); var root = Root ?? new Sequence(); var identity = new WorkflowIdentity(definitionId, Version, id); var publication = WorkflowPublication.LatestAndPublished; var name = string.IsNullOrEmpty(Name) ? definitionId : Name; var workflowMetadata = new WorkflowMetadata(name, Description); - var workflow = new Workflow(identity, publication, workflowMetadata, WorkflowOptions, root, Variables, Inputs, Outputs, Outcomes, CustomProperties, IsReadonly, IsSystem); + var workflow = new Workflow(identity, publication, workflowMetadata, WorkflowOptions, root, Variables, Inputs, Outputs, Outcomes, CustomProperties, PropertyBag, IsReadonly, IsSystem); // If a Result variable is defined, install it into the workflow, so we can capture the output into it. if (Result != null) @@ -232,15 +220,15 @@ public class WorkflowBuilder : IWorkflowBuilder workflow.Result = new Output(Result); } - var graph = await _activityVisitor.VisitAsync(workflow, cancellationToken); + var graph = await activityVisitor.VisitAsync(workflow, cancellationToken); var nodes = graph.Flatten().ToList(); // Register all activity types first. The identity graph service will need to know about all activity types. var distinctActivityTypes = nodes.Select(x => x.Activity.GetType()).Distinct().ToList(); - await _activityRegistry.RegisterAsync(distinctActivityTypes, cancellationToken); + await activityRegistry.RegisterAsync(distinctActivityTypes, cancellationToken); // Assign identities to all activities. - await _identityGraphService.AssignIdentitiesAsync(nodes); + await identityGraphService.AssignIdentitiesAsync(nodes); // Give unnamed variables in each variable container a predictable name. var variableContainers = nodes.Where(x => x.Activity is IVariableContainer).Select(x => (IVariableContainer)x.Activity).ToList(); diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowBuilder.cs b/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowBuilder.cs index b27933ab1..57bf044a3 100644 --- a/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowBuilder.cs +++ b/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowBuilder.cs @@ -1,3 +1,4 @@ +using Elsa.Common.Models; using Elsa.Workflows.Activities; using Elsa.Workflows.Memory; using Elsa.Workflows.Models; @@ -82,8 +83,14 @@ public interface IWorkflowBuilder /// /// A set of properties that can be used for storing application-specific information about the workflow being built. /// + [Obsolete("Use PropertyBag instead")] IDictionary CustomProperties { get; } + /// + /// A set of properties that can be used for storing application-specific information about the workflow being built. + /// + PropertyBag PropertyBag { get; set; } + /// /// A fluent method for setting the property. /// diff --git a/src/modules/Elsa.Workflows.Management/Entities/WorkflowDefinition.cs b/src/modules/Elsa.Workflows.Management/Entities/WorkflowDefinition.cs index 7c6f030f3..9405a4dc8 100644 --- a/src/modules/Elsa.Workflows.Management/Entities/WorkflowDefinition.cs +++ b/src/modules/Elsa.Workflows.Management/Entities/WorkflowDefinition.cs @@ -1,4 +1,5 @@ using Elsa.Common.Entities; +using Elsa.Common.Models; using Elsa.Workflows.Memory; using Elsa.Workflows.Models; @@ -54,11 +55,12 @@ public class WorkflowDefinition : VersionedEntity /// public ICollection Outcomes { get; set; } = new List(); - /// /// Stores custom information about the workflow. Can be used to store application-specific properties to associate with the workflow. - /// public IDictionary CustomProperties { get; set; } = new Dictionary(); + /// Stores custom information about the workflow. Can be used to store application-specific properties to associate with the workflow. + public PropertyBag PropertyBag { get; set; } = new(); + /// /// The name of the workflow provider that created this workflow, if any. /// @@ -98,6 +100,5 @@ public class WorkflowDefinition : VersionedEntity /// /// Creates and returns a shallow copy of the workflow definition. /// - /// public WorkflowDefinition ShallowClone() => (WorkflowDefinition)MemberwiseClone(); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Mappers/WorkflowDefinitionMapper.cs b/src/modules/Elsa.Workflows.Management/Mappers/WorkflowDefinitionMapper.cs index a090db733..9f6ac512a 100644 --- a/src/modules/Elsa.Workflows.Management/Mappers/WorkflowDefinitionMapper.cs +++ b/src/modules/Elsa.Workflows.Management/Mappers/WorkflowDefinitionMapper.cs @@ -46,6 +46,7 @@ public class WorkflowDefinitionMapper source.Outputs, source.Outcomes, source.CustomProperties, + source.PropertyBag, source.IsReadonly, source.IsSystem); } @@ -78,6 +79,7 @@ public class WorkflowDefinitionMapper source.Outputs ?? new List(), source.Outcomes ?? new List(), source.CustomProperties ?? new Dictionary(), + source.PropertyBag ?? new(), source.IsReadonly, source.IsSystem); } @@ -116,6 +118,7 @@ public class WorkflowDefinitionMapper workflowDefinition.Outputs, workflowDefinition.Outcomes, workflowDefinition.CustomProperties, + workflowDefinition.PropertyBag, workflowDefinition.IsReadonly, workflowDefinition.IsSystem, workflowDefinition.IsLatest, @@ -147,6 +150,7 @@ public class WorkflowDefinitionMapper workflow.Outputs, workflow.Outcomes, workflow.CustomProperties, + workflow.PropertyBag, workflow.IsReadonly, workflow.IsSystem, workflow.Publication.IsLatest, diff --git a/src/modules/Elsa.Workflows.Management/Models/WorkflowDefinitionModel.cs b/src/modules/Elsa.Workflows.Management/Models/WorkflowDefinitionModel.cs index f29087dcc..5f0332486 100644 --- a/src/modules/Elsa.Workflows.Management/Models/WorkflowDefinitionModel.cs +++ b/src/modules/Elsa.Workflows.Management/Models/WorkflowDefinitionModel.cs @@ -1,12 +1,11 @@ +using Elsa.Common.Models; using Elsa.Workflows.Contracts; using Elsa.Workflows.Models; using JetBrains.Annotations; namespace Elsa.Workflows.Management.Models; -/// /// Represents a serializable workflow definition. -/// [PublicAPI] public record WorkflowDefinitionModel( string Id, @@ -20,7 +19,9 @@ public record WorkflowDefinitionModel( ICollection? Inputs, ICollection? Outputs, ICollection? Outcomes, + [property: Obsolete("Use PropertyBag instead")] IDictionary? CustomProperties, + PropertyBag? PropertyBag, bool IsReadonly, bool IsSystem, bool IsLatest, @@ -44,7 +45,8 @@ public record WorkflowDefinitionModel( default!, default!, default!, - default!, + default, + default, default!, default!, default!, diff --git a/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionManager.cs b/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionManager.cs index 657f72528..5bea2d957 100644 --- a/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionManager.cs +++ b/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionManager.cs @@ -96,7 +96,7 @@ public class WorkflowDefinitionManager : IWorkflowDefinitionManager { if (definitionToDelete.IsPublished) { - throw new Exception("Published version cannot be deleted before retracting it"); + await _workflowPublisher.RetractAsync(definitionToDelete, cancellationToken); } await _notificationSender.SendAsync(new WorkflowDefinitionVersionDeleting(definitionToDelete), cancellationToken);