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