diff --git a/.github/workflows/elsa-server-and-studio.yml b/.github/workflows/elsa-server-and-studio.yml index 301c18a4a..9c23cc7c9 100644 --- a/.github/workflows/elsa-server-and-studio.yml +++ b/.github/workflows/elsa-server-and-studio.yml @@ -29,7 +29,7 @@ jobs: with: # list of Docker images to use as base name for tags images: | - elsaworkflows/elsa-server-and-studio-v3-3-0-preview + elsaworkflows/elsa-server-and-studio-v3-4-0-preview flavor: | latest=true # generate Docker tags based on the following events/attributes diff --git a/.github/workflows/elsa-server.yml b/.github/workflows/elsa-server.yml index 275cf6147..289332ba4 100644 --- a/.github/workflows/elsa-server.yml +++ b/.github/workflows/elsa-server.yml @@ -29,7 +29,7 @@ jobs: with: # list of Docker images to use as base name for tags images: | - elsaworkflows/elsa-server-v3-3-0-preview + elsaworkflows/elsa-server-v3-4-0-preview flavor: | latest=true # generate Docker tags based on the following events/attributes diff --git a/.github/workflows/elsa-studio.yml b/.github/workflows/elsa-studio.yml index 3c5079b5c..f2fe2bff2 100644 --- a/.github/workflows/elsa-studio.yml +++ b/.github/workflows/elsa-studio.yml @@ -29,7 +29,7 @@ jobs: with: # list of Docker images to use as base name for tags images: | - elsaworkflows/elsa-studio-v3-3-0-preview + elsaworkflows/elsa-studio-v3-4-0-preview flavor: | latest=true # generate Docker tags based on the following events/attributes diff --git a/src/apps/Elsa.Server.Web/Messages/OrderReceived.cs b/src/apps/Elsa.Server.Web/Messages/OrderReceived.cs index 411081bd1..32151ae81 100644 --- a/src/apps/Elsa.Server.Web/Messages/OrderReceived.cs +++ b/src/apps/Elsa.Server.Web/Messages/OrderReceived.cs @@ -2,5 +2,5 @@ namespace Elsa.Server.Web.Messages; public class OrderReceived { - public string OrderId { get; set; } = default!; + public string OrderId { get; set; } = null!; } \ No newline at end of file diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index 57839e56e..63b49f2ff 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -2,6 +2,7 @@ using System.Text.Encodings.Web; using Elsa.Alterations.Extensions; using Elsa.Alterations.MassTransit.Extensions; using Elsa.Caching.Options; +using Elsa.Common.Codecs; using Elsa.Common.DistributedHosting.DistributedLocks; using Elsa.Common.RecurringTasks; using Elsa.Common.Serialization; @@ -51,7 +52,6 @@ using Elsa.Workflows.CommitStates.Strategies; using Elsa.Workflows.IncidentStrategies; using Elsa.Workflows.LogPersistence; using Elsa.Workflows.Management; -using Elsa.Workflows.Management.Compression; using Elsa.Workflows.Management.Stores; using Elsa.Workflows.Memory; using Elsa.Workflows.Options; @@ -86,7 +86,7 @@ using StackExchange.Redis; // ReSharper disable RedundantAssignment const PersistenceProvider persistenceProvider = PersistenceProvider.EntityFrameworkCore; -const bool useDbContextPooling = true; +const bool useDbContextPooling = false; const bool useHangfire = false; const bool useQuartz = true; const bool useMassTransit = true; @@ -107,7 +107,7 @@ const bool disableVariableWrappers = false; const bool disableVariableCopying = false; const bool useManualOtelInstrumentation = false; -ObjectConverter.StrictMode = true; // Default. +ObjectConverter.StrictMode = false; var builder = WebApplication.CreateBuilder(args); var services = builder.Services; @@ -560,6 +560,7 @@ services elsa.UseMassTransit(massTransit => { massTransit.DisableConsumers = appRole == ApplicationRole.Api; + massTransit.AddMessageType(); if (massTransitBroker == MassTransitBroker.AzureServiceBus) { diff --git a/src/clients/Elsa.Api.Client/Extensions/DependencyInjectionExtensions.cs b/src/clients/Elsa.Api.Client/Extensions/DependencyInjectionExtensions.cs index 3e23b2e45..e6d846919 100644 --- a/src/clients/Elsa.Api.Client/Extensions/DependencyInjectionExtensions.cs +++ b/src/clients/Elsa.Api.Client/Extensions/DependencyInjectionExtensions.cs @@ -2,6 +2,7 @@ using Elsa.Api.Client.Options; using Elsa.Api.Client.Resources.ActivityDescriptorOptions.Contracts; using Elsa.Api.Client.Resources.ActivityDescriptors.Contracts; using Elsa.Api.Client.Resources.ActivityExecutions.Contracts; +using Elsa.Api.Client.Resources.Alterations.Contracts; using Elsa.Api.Client.Resources.CommitStrategies.Contracts; using Elsa.Api.Client.Resources.Features.Contracts; using Elsa.Api.Client.Resources.Identity.Contracts; @@ -80,6 +81,7 @@ public static class DependencyInjectionExtensions services.AddApi(builderOptions); services.AddApi(builderOptions); services.AddApi(builderOptions); + services.AddApi(builderOptions); services.AddApi(builderOptions); }); } diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Contracts/IAlteration.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Contracts/IAlteration.cs deleted file mode 100644 index 0ce1f7a50..000000000 --- a/src/clients/Elsa.Api.Client/Resources/Alterations/Contracts/IAlteration.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Elsa.Api.Client.Resources.Alterations.Contracts; - -/// -/// Marker interface for all alteration classes -/// -public interface IAlteration; \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationBase.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationBase.cs deleted file mode 100644 index 18615eca9..000000000 --- a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationBase.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Elsa.Api.Client.Resources.Alterations.Contracts; - -namespace Elsa.Api.Client.Resources.Alterations.Models; - -/// -/// A base class for all IAlterations. -/// -public abstract class AlterationBase : IAlteration; \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationPlan.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationPlan.cs index 993858cf5..dd5843d5f 100644 --- a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationPlan.cs +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationPlan.cs @@ -1,4 +1,4 @@ -using Elsa.Api.Client.Resources.Alterations.Contracts; +using System.Text.Json.Nodes; using Elsa.Api.Client.Resources.Alterations.Enums; using Elsa.Api.Client.Shared.Models; @@ -12,7 +12,7 @@ public class AlterationPlan : Entity /// /// The alterations to be applied. /// - public ICollection Alterations { get; set; } = new List(); + public ICollection Alterations { get; set; } = new List(); /// /// The IDs of the workflow instances that this plan applies to. diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationPlanParams.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationPlanParams.cs index f9fc5a1c1..893aa89e6 100644 --- a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationPlanParams.cs +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationPlanParams.cs @@ -1,4 +1,4 @@ -using Elsa.Api.Client.Resources.Alterations.Contracts; +using System.Text.Json.Nodes; namespace Elsa.Api.Client.Resources.Alterations.Models; @@ -15,7 +15,7 @@ public class AlterationPlanParams /// /// The alterations to be applied. /// - public ICollection Alterations { get; set; } = new List(); + public ICollection Alterations { get; set; } = new List(); /// /// The IDs of the workflow instances that this plan applies to. diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/Cancel.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/Cancel.cs deleted file mode 100644 index c616d4c3a..000000000 --- a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/Cancel.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Elsa.Api.Client.Resources.Alterations.Models; - -/// -/// Cancels the workflow instances in an alteration plan. -/// -public class Cancel : AlterationBase; \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/CancelActivity.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/CancelActivity.cs deleted file mode 100644 index 2500e7d97..000000000 --- a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/CancelActivity.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Elsa.Api.Client.Resources.Alterations.Models; - -/// -/// Cancels a workflow instance activity during an alteration -/// -public class CancelActivity : AlterationBase -{ - /// - /// The ID of the activity to be cancelled. If not specified, the activity instance ID will be used. - /// - public string? ActivityId { get; set; } - - /// - /// The ID of the activity instance to be cancelled. If specified, overrides . - /// - public string? ActivityInstanceId { get; set; } -} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/Migrate.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/Migrate.cs deleted file mode 100644 index 1a4dc3b69..000000000 --- a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/Migrate.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Elsa.Api.Client.Resources.Alterations.Models; - -/// -/// Migrates a workflow instance to a newer version in an alteration. -/// -public class Migrate : AlterationBase -{ - /// - /// The target version to upgrade to. - /// - public int TargetVersion { get; set; } -} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/ModifyVariable.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/ModifyVariable.cs deleted file mode 100644 index 60ab9f6ba..000000000 --- a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/ModifyVariable.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Elsa.Api.Client.Resources.Alterations.Models; - -/// -/// Modifies a variable in a workflow instance alteration -/// -public class ModifyVariable : AlterationBase -{ - /// - /// The ID of the variable to modify. - /// - public string VariableId { get; set; } = default!; - - /// - /// The new value of the variable. - /// - public object? Value { get; set; } - -} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/ScheduleActivity.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/ScheduleActivity.cs deleted file mode 100644 index 2b0f77f6d..000000000 --- a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/ScheduleActivity.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Elsa.Api.Client.Resources.Alterations.Models; - -/// -/// Schedules an activity for execution in an alteration. -/// -public class ScheduleActivity : AlterationBase -{ - /// - /// The ID of the next activity to be scheduled. If not specified, the ActivityInstanceId will be used. - /// - public string? ActivityId { get; set; } - - /// - /// The ID of the activity instance to be scheduled. If not specified, the ActivityId will be used. - /// - public string? ActivityInstanceId { get; set; } -} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Responses/RunRequest.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Responses/RunRequest.cs index fdaf341e8..0d9995712 100644 --- a/src/clients/Elsa.Api.Client/Resources/Alterations/Responses/RunRequest.cs +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Responses/RunRequest.cs @@ -1,4 +1,4 @@ -using Elsa.Api.Client.Resources.Alterations.Contracts; +using System.Text.Json.Nodes; namespace Elsa.Api.Client.Resources.Alterations.Responses; @@ -10,7 +10,7 @@ public class RunRequest /// /// The alterations to be applied. /// - public ICollection Alterations { get; set; } = new List(); + public ICollection Alterations { get; set; } = new List(); /// /// The IDs of the workflow instances that this plan applies to. diff --git a/src/modules/Elsa.Alterations/Handlers/AlterationPlanCompletedHandler.cs b/src/modules/Elsa.Alterations/Handlers/AlterationPlanCompletedHandler.cs index 18a210510..f9b605055 100644 --- a/src/modules/Elsa.Alterations/Handlers/AlterationPlanCompletedHandler.cs +++ b/src/modules/Elsa.Alterations/Handlers/AlterationPlanCompletedHandler.cs @@ -1,10 +1,9 @@ using Elsa.Alterations.Bookmarks; using Elsa.Alterations.Core.Notifications; using Elsa.Mediator.Contracts; +using Elsa.Workflows; using Elsa.Workflows.Helpers; using Elsa.Workflows.Runtime; -using Elsa.Workflows.Runtime.Contracts; -using Elsa.Workflows.Runtime.Requests; using JetBrains.Annotations; namespace Elsa.Alterations.Handlers; @@ -13,7 +12,7 @@ namespace Elsa.Alterations.Handlers; /// Handles notifications and triggers any workflows that are waiting for the plan to complete. /// [UsedImplicitly] -public class AlterationPlanCompletedHandler(IWorkflowDispatcher workflowDispatcher) : INotificationHandler +public class AlterationPlanCompletedHandler(IBookmarkQueue bookmarkQueue, IStimulusHasher stimulusHasher) : INotificationHandler { /// public async Task HandleAsync(AlterationPlanCompleted notification, CancellationToken cancellationToken) @@ -21,7 +20,12 @@ public class AlterationPlanCompletedHandler(IWorkflowDispatcher workflowDispatch // Trigger any workflow instances that are waiting for the plan to complete. var planId = notification.Plan.Id; var bookmarkPayload = new AlterationPlanCompletedPayload(planId); - var triggerRequest = new DispatchTriggerWorkflowsRequest(ActivityTypeNameHelper.GenerateTypeName(), bookmarkPayload); - await workflowDispatcher.DispatchAsync(triggerRequest, cancellationToken); + var activityTypeName = ActivityTypeNameHelper.GenerateTypeName(); + var item = new NewBookmarkQueueItem + { + ActivityTypeName = activityTypeName, + StimulusHash = stimulusHasher.Hash(activityTypeName, bookmarkPayload) + }; + await bookmarkQueue.EnqueueAsync(item, cancellationToken); } } \ No newline at end of file diff --git a/src/modules/Elsa.Alterations/Services/AlterationPlanManager.cs b/src/modules/Elsa.Alterations/Services/AlterationPlanManager.cs index eb40d5d7c..23e054216 100644 --- a/src/modules/Elsa.Alterations/Services/AlterationPlanManager.cs +++ b/src/modules/Elsa.Alterations/Services/AlterationPlanManager.cs @@ -25,7 +25,7 @@ public class AlterationPlanManager(IAlterationPlanStore planStore, IAlterationJo var jobFilter = new AlterationJobFilter { PlanId = planId, - Statuses = new[] { AlterationJobStatus.Pending, AlterationJobStatus.Running } + Statuses = [AlterationJobStatus.Pending, AlterationJobStatus.Running] }; return await jobStore.CountAsync(jobFilter, cancellationToken) == 0; diff --git a/src/modules/Elsa.Alterations/Services/DefaultAlterationPlanScheduler.cs b/src/modules/Elsa.Alterations/Services/DefaultAlterationPlanScheduler.cs index fd8275282..4ab30f434 100644 --- a/src/modules/Elsa.Alterations/Services/DefaultAlterationPlanScheduler.cs +++ b/src/modules/Elsa.Alterations/Services/DefaultAlterationPlanScheduler.cs @@ -42,7 +42,7 @@ public class DefaultAlterationPlanScheduler : IAlterationPlanScheduler var workflowGraph = await _workflowDefinitionService.FindWorkflowGraphAsync(definitionId, VersionOptions.Published, cancellationToken); if (workflowGraph == null) - throw new Exception($"Workflow definition with ID '{definitionId}' not found"); + throw new($"Workflow definition with ID '{definitionId}' not found"); var serializedPlan = _jsonSerializer.Serialize(planParams); var request = new DispatchWorkflowDefinitionRequest(workflowGraph.Workflow.Identity.Id) diff --git a/src/modules/Elsa.Workflows.Management/Compression/GZip.cs b/src/modules/Elsa.Common/Codecs/GZip.cs similarity index 96% rename from src/modules/Elsa.Workflows.Management/Compression/GZip.cs rename to src/modules/Elsa.Common/Codecs/GZip.cs index d00b5b819..b7b3d1eda 100644 --- a/src/modules/Elsa.Workflows.Management/Compression/GZip.cs +++ b/src/modules/Elsa.Common/Codecs/GZip.cs @@ -1,7 +1,7 @@ using System.IO.Compression; using System.Text; -namespace Elsa.Workflows.Management.Compression; +namespace Elsa.Common.Codecs; /// /// Represents a GZip compression strategy. diff --git a/src/modules/Elsa.Workflows.Management/Compression/None.cs b/src/modules/Elsa.Common/Codecs/None.cs similarity index 91% rename from src/modules/Elsa.Workflows.Management/Compression/None.cs rename to src/modules/Elsa.Common/Codecs/None.cs index 48c41c8e9..5e2dc800b 100644 --- a/src/modules/Elsa.Workflows.Management/Compression/None.cs +++ b/src/modules/Elsa.Common/Codecs/None.cs @@ -1,4 +1,4 @@ -namespace Elsa.Workflows.Management.Compression; +namespace Elsa.Common.Codecs; /// /// Represents a compression strategy that does not compress or decompress the input. diff --git a/src/modules/Elsa.Workflows.Management/Compression/Zstd.cs b/src/modules/Elsa.Common/Codecs/Zstd.cs similarity index 95% rename from src/modules/Elsa.Workflows.Management/Compression/Zstd.cs rename to src/modules/Elsa.Common/Codecs/Zstd.cs index cb5fd9494..11a49c5d6 100644 --- a/src/modules/Elsa.Workflows.Management/Compression/Zstd.cs +++ b/src/modules/Elsa.Common/Codecs/Zstd.cs @@ -1,7 +1,7 @@ using System.Text; using IronCompress; -namespace Elsa.Workflows.Management.Compression; +namespace Elsa.Common.Codecs; /// /// Represents a ZSTD compression strategy. diff --git a/src/modules/Elsa.Workflows.Management/Contracts/ICompressionCodec.cs b/src/modules/Elsa.Common/Contracts/ICompressionCodec.cs similarity index 92% rename from src/modules/Elsa.Workflows.Management/Contracts/ICompressionCodec.cs rename to src/modules/Elsa.Common/Contracts/ICompressionCodec.cs index 82cb7e402..c34a0dc3d 100644 --- a/src/modules/Elsa.Workflows.Management/Contracts/ICompressionCodec.cs +++ b/src/modules/Elsa.Common/Contracts/ICompressionCodec.cs @@ -1,4 +1,4 @@ -namespace Elsa.Workflows.Management; +namespace Elsa.Common; /// /// Represents a compression strategy. diff --git a/src/modules/Elsa.Workflows.Management/Contracts/ICompressionCodecResolver.cs b/src/modules/Elsa.Common/Contracts/ICompressionCodecResolver.cs similarity index 88% rename from src/modules/Elsa.Workflows.Management/Contracts/ICompressionCodecResolver.cs rename to src/modules/Elsa.Common/Contracts/ICompressionCodecResolver.cs index 852b95ab0..86494155c 100644 --- a/src/modules/Elsa.Workflows.Management/Contracts/ICompressionCodecResolver.cs +++ b/src/modules/Elsa.Common/Contracts/ICompressionCodecResolver.cs @@ -1,4 +1,4 @@ -namespace Elsa.Workflows.Management; +namespace Elsa.Common; /// /// Resolves a from its name. diff --git a/src/modules/Elsa.Common/Elsa.Common.csproj b/src/modules/Elsa.Common/Elsa.Common.csproj index 654e988e5..9b5d15779 100644 --- a/src/modules/Elsa.Common/Elsa.Common.csproj +++ b/src/modules/Elsa.Common/Elsa.Common.csproj @@ -16,6 +16,7 @@ + diff --git a/src/modules/Elsa.Workflows.Core/Extensions/StringExtensions.cs b/src/modules/Elsa.Common/Extensions/StringExtensions.cs similarity index 100% rename from src/modules/Elsa.Workflows.Core/Extensions/StringExtensions.cs rename to src/modules/Elsa.Common/Extensions/StringExtensions.cs diff --git a/src/modules/Elsa.Common/Features/CompressionFeature.cs b/src/modules/Elsa.Common/Features/CompressionFeature.cs new file mode 100644 index 000000000..f6b5955c1 --- /dev/null +++ b/src/modules/Elsa.Common/Features/CompressionFeature.cs @@ -0,0 +1,21 @@ +using Elsa.Common.Codecs; +using Elsa.Common.Services; +using Elsa.Features.Abstractions; +using Elsa.Features.Services; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Common.Features; + +[UsedImplicitly] +public class CompressionFeature(IModule module) : FeatureBase(module) +{ + public override void Apply() + { + Services + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Common/Multitenancy/Entities/Tenant.cs b/src/modules/Elsa.Common/Multitenancy/Entities/Tenant.cs index a832baa2c..cba4b371b 100644 --- a/src/modules/Elsa.Common/Multitenancy/Entities/Tenant.cs +++ b/src/modules/Elsa.Common/Multitenancy/Entities/Tenant.cs @@ -13,7 +13,7 @@ public class Tenant : Entity /// /// Gets or sets the name. /// - public string Name { get; set; } = default!; + public string Name { get; set; } = null!; /// /// Gets or sets the configuration. @@ -22,7 +22,7 @@ public class Tenant : Entity public static readonly Tenant Default = new() { - Id = string.Empty, + Id = null!, Name = "Default" }; } diff --git a/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs b/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs index de7f75edd..89298e5ac 100644 --- a/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs +++ b/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs @@ -1,3 +1,4 @@ +using Elsa.Extensions; using Microsoft.Extensions.DependencyInjection; namespace Elsa.Common.Multitenancy; @@ -19,7 +20,7 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop public async Task FindAsync(string id, CancellationToken cancellationToken = default) { var dictionary = await GetTenantsDictionaryAsync(cancellationToken); - return dictionary.TryGetValue(id, out var tenant) ? tenant : null; + return dictionary.TryGetValue(id.EmptyIfNull(), out var tenant) ? tenant : null; } public async Task FindAsync(TenantFilter filter, CancellationToken cancellationToken = default) @@ -31,7 +32,7 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop public async Task GetAsync(string id, CancellationToken cancellationToken = default) { var dictionary = await GetTenantsDictionaryAsync(cancellationToken); - return dictionary[id]; + return dictionary[id.EmptyIfNull()]; } public async Task GetAsync(TenantFilter filter, CancellationToken cancellationToken = default) @@ -76,7 +77,7 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop var tenantsProvider = scope.ServiceProvider.GetRequiredService(); var currentTenants = await GetTenantsDictionaryAsync(cancellationToken); var currentTenantIds = currentTenants.Keys; - var newTenants = (await tenantsProvider.ListAsync(cancellationToken)).ToDictionary(x => x.Id); + var newTenants = (await tenantsProvider.ListAsync(cancellationToken)).ToDictionary(x => x.Id.EmptyIfNull()); var newTenantIds = newTenants.Keys; var removedTenantIds = currentTenantIds.Except(newTenantIds).ToArray(); var addedTenantIds = newTenantIds.Except(currentTenantIds).ToArray(); @@ -129,7 +130,7 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop private async Task RegisterTenantAsync(Tenant tenant, CancellationToken cancellationToken = default) { var scope = tenantScopeFactory.CreateScope(tenant); - _tenantsDictionary![tenant.Id] = tenant; + _tenantsDictionary![tenant.Id.EmptyIfNull()] = tenant; _tenantScopesDictionary![tenant] = scope; using (tenantAccessor.PushContext(tenant)) @@ -140,7 +141,7 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop { if (_tenantScopesDictionary!.Remove(tenant, out var scope)) { - _tenantsDictionary!.Remove(tenant.Id, out _); + _tenantsDictionary!.Remove(tenant.Id.EmptyIfNull(), out _); using (tenantAccessor.PushContext(tenant)) await tenantEvents.TenantDeactivatedAsync(new(tenant, scope, cancellationToken)); diff --git a/src/modules/Elsa.Common/Multitenancy/Models/TenantFilter.cs b/src/modules/Elsa.Common/Multitenancy/Models/TenantFilter.cs index 1fc1c1fa1..51a88590a 100644 --- a/src/modules/Elsa.Common/Multitenancy/Models/TenantFilter.cs +++ b/src/modules/Elsa.Common/Multitenancy/Models/TenantFilter.cs @@ -11,7 +11,7 @@ public class TenantFilter /// /// Gets or sets the tenant ID to filter for. /// - public string? Id { get; set; } + public string Id { get; set; } = null!; /// /// Applies the filter to the specified queryable. @@ -20,7 +20,7 @@ public class TenantFilter /// The filtered queryable. public IQueryable Apply(IQueryable queryable) { - if (Id != null) queryable = queryable.Where(x => x.Id == Id); + queryable = queryable.Where(x => x.Id == Id); return queryable; } diff --git a/src/modules/Elsa.Workflows.Management/Services/CompressionCodecResolver.cs b/src/modules/Elsa.Common/Services/CompressionCodecResolver.cs similarity index 86% rename from src/modules/Elsa.Workflows.Management/Services/CompressionCodecResolver.cs rename to src/modules/Elsa.Common/Services/CompressionCodecResolver.cs index 77b7bc77e..1cd3a1156 100644 --- a/src/modules/Elsa.Workflows.Management/Services/CompressionCodecResolver.cs +++ b/src/modules/Elsa.Common/Services/CompressionCodecResolver.cs @@ -1,6 +1,6 @@ -using Elsa.Workflows.Management.Compression; +using Elsa.Common.Codecs; -namespace Elsa.Workflows.Management.Services; +namespace Elsa.Common.Services; /// public class CompressionCodecResolver : ICompressionCodecResolver diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/Elsa.EntityFrameworkCore.Common.csproj b/src/modules/Elsa.EntityFrameworkCore.Common/Elsa.EntityFrameworkCore.Common.csproj index e3f4635d9..3454db95b 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/Elsa.EntityFrameworkCore.Common.csproj +++ b/src/modules/Elsa.EntityFrameworkCore.Common/Elsa.EntityFrameworkCore.Common.csproj @@ -14,6 +14,7 @@ + diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs b/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs index 5329a7183..2a22f2a7d 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs @@ -1,8 +1,8 @@ using Elsa.Common.Entities; using Elsa.Common.Multitenancy; +using Elsa.Extensions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; -using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.DependencyInjection; namespace Elsa.EntityFrameworkCore; @@ -50,7 +50,7 @@ public abstract class ElsaDbContextBase : DbContext, IElsaDbContextSchema var tenantId = tenantAccessor?.Tenant?.Id; if (!string.IsNullOrWhiteSpace(tenantId)) - TenantId = tenantId; + TenantId = tenantId.NullIfEmpty(); } /// diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextOptionsExtension.cs b/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextOptionsExtension.cs index e59376a64..3bd867eeb 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextOptionsExtension.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextOptionsExtension.cs @@ -33,6 +33,5 @@ public class ElsaDbContextOptionsExtension : IDbContextOptionsExtension /// public void Validate(IDbContextOptions options) { - } } \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/EntityHandlers/ApplyTenantId.cs b/src/modules/Elsa.EntityFrameworkCore.Common/EntityHandlers/ApplyTenantId.cs index 7167f46d6..b55f9af65 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/EntityHandlers/ApplyTenantId.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/EntityHandlers/ApplyTenantId.cs @@ -1,4 +1,5 @@ using Elsa.Common.Entities; +using Elsa.Extensions; using Microsoft.EntityFrameworkCore.ChangeTracking; namespace Elsa.EntityFrameworkCore.EntityHandlers; @@ -12,7 +13,7 @@ public class ApplyTenantId : IEntitySavingHandler public ValueTask HandleAsync(ElsaDbContextBase dbContext, EntityEntry entry, CancellationToken cancellationToken = default) { if (entry.Entity is Entity entity) - entity.TenantId = dbContext.TenantId; + entity.TenantId = dbContext.TenantId.NullIfEmpty(); return default; } diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/EntityHandlers/SetTenantIdFilter.cs b/src/modules/Elsa.EntityFrameworkCore.Common/EntityHandlers/SetTenantIdFilter.cs index 0a352aabe..0e5bb9d9d 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/EntityHandlers/SetTenantIdFilter.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/EntityHandlers/SetTenantIdFilter.cs @@ -1,5 +1,6 @@ using System.Linq.Expressions; using Elsa.Common.Entities; +using Elsa.Extensions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query; @@ -17,8 +18,9 @@ public class SetTenantIdFilter : IEntityModelCreatingHandler if (!entityType.ClrType.IsAssignableTo(typeof(Entity))) return; + var tenantId = dbContext.TenantId.NullIfEmpty(); var parameter = Expression.Parameter(entityType.ClrType); - Expression> filterExpr = entity => dbContext.TenantId == entity.TenantId; + Expression> filterExpr = entity => entity.TenantId == tenantId; var body = ReplacingExpressionVisitor.Replace(filterExpr.Parameters[0], parameter, filterExpr.Body); var lambdaExpression = Expression.Lambda(body, parameter); diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/Extensions/BulkUpsertExtensions.cs b/src/modules/Elsa.EntityFrameworkCore.Common/Extensions/BulkUpsertExtensions.cs index b82b3fe6a..076b685d3 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/Extensions/BulkUpsertExtensions.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/Extensions/BulkUpsertExtensions.cs @@ -154,9 +154,7 @@ public static class BulkUpsertExtensions var entityType = dbContext.Model.FindEntityType(typeof(TEntity))!; var tableName = entityType.GetTableName(); var storeObject = StoreObjectIdentifier.Table(tableName!, entityType.GetSchema()); - var props = entityType.GetProperties().ToList(); - var keyProp = entityType.FindProperty(keySelector.GetMemberAccess().Name)!; var keyColumnName = keyProp.GetColumnName(storeObject); var columnNames = props diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/PersistenceFeatureBase.cs b/src/modules/Elsa.EntityFrameworkCore.Common/PersistenceFeatureBase.cs index 68d7c300b..1fd3352c0 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/PersistenceFeatureBase.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/PersistenceFeatureBase.cs @@ -60,6 +60,8 @@ public abstract class PersistenceFeatureBase(IModule modul Services.AddPooledDbContextFactory(setup); else Services.AddDbContextFactory(setup, DbContextFactoryLifetime); + + Services.Decorate, TenantAwareDbContextFactory>(); } protected virtual void ConfigureMigrations() diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/Store.cs b/src/modules/Elsa.EntityFrameworkCore.Common/Store.cs index 0a353a20e..021072f07 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/Store.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/Store.cs @@ -1,7 +1,9 @@ using System.Linq.Expressions; using Elsa.Common.Entities; using Elsa.Common.Models; +using Elsa.Common.Multitenancy; using Elsa.EntityFrameworkCore.Extensions; +using Elsa.Extensions; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -76,7 +78,7 @@ public class Store(IDbContextFactory dbContextF /// The cancellation token. public async Task AddManyAsync( IEnumerable entities, - Func? onSaving = default, + Func? onSaving = null, CancellationToken cancellationToken = default) { var entityList = entities.ToList(); @@ -151,7 +153,7 @@ public class Store(IDbContextFactory dbContextF /// The entities to save. /// The key selector to get the primary key property. /// The cancellation token. - public async Task SaveManyAsync(IEnumerable entities, Expression> keySelector, CancellationToken cancellationToken = default) => await SaveManyAsync(entities, keySelector, default, cancellationToken); + public async Task SaveManyAsync(IEnumerable entities, Expression> keySelector, CancellationToken cancellationToken = default) => await SaveManyAsync(entities, keySelector, null, cancellationToken); /// /// Saves the specified entities. @@ -163,7 +165,7 @@ public class Store(IDbContextFactory dbContextF public async Task SaveManyAsync( IEnumerable entities, Expression> keySelector, - Func? onSaving = default, + Func? onSaving = null, CancellationToken cancellationToken = default) { var entityList = entities.ToList(); @@ -179,6 +181,14 @@ public class Store(IDbContextFactory dbContextF await Task.WhenAll(savingTasks); } + // When doing a custom SQL query (Bulk Upsert), none of the installed query filters will be applied. Hence, we are assigning the current tenant ID explicitly. + var tenantId = serviceProvider.GetRequiredService().Tenant?.Id.NullIfEmpty(); + foreach (var entity in entityList) + { + if (entity is Entity entityWithTenant) + entityWithTenant.TenantId = tenantId; + } + try { await dbContext.BulkUpsertAsync(entityList, keySelector, cancellationToken); @@ -237,9 +247,9 @@ public class Store(IDbContextFactory dbContextF await using var dbContext = await CreateDbContextAsync(cancellationToken); dbContext.Attach(entity); - foreach (var property in properties) + foreach (var property in properties) dbContext.Entry(entity).Property(property).IsModified = true; - + await dbContext.SaveChangesAsync(cancellationToken); } @@ -249,7 +259,7 @@ public class Store(IDbContextFactory dbContextF /// The predicate to use. /// The cancellation token. /// The entity if found, otherwise null. - public async Task FindAsync(Expression> predicate, CancellationToken cancellationToken = default) => await FindAsync(predicate, default, cancellationToken); + public async Task FindAsync(Expression> predicate, CancellationToken cancellationToken = default) => await FindAsync(predicate, null, cancellationToken); /// /// Finds the entity matching the specified predicate. @@ -258,7 +268,7 @@ public class Store(IDbContextFactory dbContextF /// A callback to run after the entity is loaded /// The cancellation token. /// - public async Task FindAsync(Expression> predicate, Func? onLoading = default, CancellationToken cancellationToken = default) + public async Task FindAsync(Expression> predicate, Func? onLoading = null, CancellationToken cancellationToken = default) { await using var dbContext = await CreateDbContextAsync(cancellationToken); var set = dbContext.Set().AsNoTracking(); @@ -267,7 +277,7 @@ public class Store(IDbContextFactory dbContextF if (entity == null) return null; - if (onLoading != default) + if (onLoading != null) entity = onLoading.Invoke(dbContext, entity); return entity; @@ -280,7 +290,7 @@ public class Store(IDbContextFactory dbContextF /// A callback to run after the entity is loaded /// The cancellation token /// The entity if found, otherwise null - public async Task FindAsync(Func, IQueryable> query, Func? onLoading = default, CancellationToken cancellationToken = default) + public async Task FindAsync(Func, IQueryable> query, Func? onLoading = null, CancellationToken cancellationToken = default) { return await FindAsync(query, onLoading, false, cancellationToken); } @@ -293,7 +303,7 @@ public class Store(IDbContextFactory dbContextF /// Define is the request should be tenant agnostic or not /// The cancellation token /// The entity if found, otherwise null - public async Task FindAsync(Func, IQueryable> query, Func? onLoading = default, bool tenantAgnostic = false, CancellationToken cancellationToken = default) + public async Task FindAsync(Func, IQueryable> query, Func? onLoading = null, bool tenantAgnostic = false, CancellationToken cancellationToken = default) { return await QueryAsync(query, onLoading, tenantAgnostic, cancellationToken).FirstOrDefault(); } @@ -324,12 +334,12 @@ public class Store(IDbContextFactory dbContextF /// /// Finds a list of entities using a query /// - public async Task> FindManyAsync(Expression> predicate, CancellationToken cancellationToken = default) => await FindManyAsync(predicate, default, cancellationToken); + public async Task> FindManyAsync(Expression> predicate, CancellationToken cancellationToken = default) => await FindManyAsync(predicate, null, cancellationToken); /// /// Finds a list of entities using a query /// - public async Task> FindManyAsync(Expression> predicate, Action? onLoading = default, CancellationToken cancellationToken = default) + public async Task> FindManyAsync(Expression> predicate, Action? onLoading = null, CancellationToken cancellationToken = default) { await using var dbContext = await CreateDbContextAsync(cancellationToken); var set = dbContext.Set().AsNoTracking(); @@ -349,9 +359,9 @@ public class Store(IDbContextFactory dbContextF Expression> predicate, Expression> orderBy, OrderDirection orderDirection = OrderDirection.Ascending, - PageArgs? pageArgs = default, + PageArgs? pageArgs = null, CancellationToken cancellationToken = default) => - await FindManyAsync(predicate, orderBy, orderDirection, pageArgs, default, cancellationToken); + await FindManyAsync(predicate, orderBy, orderDirection, pageArgs, null, cancellationToken); /// /// Returns a list of entities using a query @@ -360,8 +370,8 @@ public class Store(IDbContextFactory dbContextF Expression>? predicate, Expression>? orderBy, OrderDirection orderDirection = OrderDirection.Ascending, - PageArgs? pageArgs = default, - Func? onLoading = default, + PageArgs? pageArgs = null, + Func? onLoading = null, CancellationToken cancellationToken = default) { await using var dbContext = await CreateDbContextAsync(cancellationToken); @@ -394,7 +404,7 @@ public class Store(IDbContextFactory dbContextF return ListAsync(null, cancellationToken); } - public async Task> ListAsync(Action? onLoading = default, CancellationToken cancellationToken = default) + public async Task> ListAsync(Action? onLoading = null, CancellationToken cancellationToken = default) { await using var dbContext = await CreateDbContextAsync(cancellationToken); var set = dbContext.Set().AsNoTracking(); @@ -447,7 +457,7 @@ public class Store(IDbContextFactory dbContextF /// public async Task> QueryAsync(Func, IQueryable> query, CancellationToken cancellationToken = default) { - return await QueryAsync(query, default, false, cancellationToken); + return await QueryAsync(query, null, false, cancellationToken); } /// @@ -455,13 +465,13 @@ public class Store(IDbContextFactory dbContextF /// public async Task> QueryAsync(Func, IQueryable> query, bool tenantAgnostic, CancellationToken cancellationToken = default) { - return await QueryAsync(query, default, tenantAgnostic, cancellationToken); + return await QueryAsync(query, null, tenantAgnostic, cancellationToken); } /// /// Queries the database using a query and a selector. /// - public async Task> QueryAsync(Func, IQueryable> query, Func? onLoading = default, CancellationToken cancellationToken = default) + public async Task> QueryAsync(Func, IQueryable> query, Func? onLoading = null, CancellationToken cancellationToken = default) { return await QueryAsync(query, onLoading, false, cancellationToken); } @@ -469,7 +479,7 @@ public class Store(IDbContextFactory dbContextF /// /// Queries the database using a query and a selector. /// - public async Task> QueryAsync(Func, IQueryable> query, Func? onLoading = default, bool ignoreQueryFilters = false, CancellationToken cancellationToken = default) + public async Task> QueryAsync(Func, IQueryable> query, Func? onLoading = null, bool ignoreQueryFilters = false, CancellationToken cancellationToken = default) { await using var dbContext = await CreateDbContextAsync(cancellationToken); var asNoTracking = onLoading == null; diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/TenantAwareDbContextFactory.cs b/src/modules/Elsa.EntityFrameworkCore.Common/TenantAwareDbContextFactory.cs new file mode 100644 index 000000000..710ae0b2d --- /dev/null +++ b/src/modules/Elsa.EntityFrameworkCore.Common/TenantAwareDbContextFactory.cs @@ -0,0 +1,37 @@ +using Elsa.Common.Multitenancy; +using Elsa.Extensions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; + +namespace Elsa.EntityFrameworkCore; + +/// +/// A factory class that wraps and extends the functionality of an existing +/// to provide multi-tenancy support. This ensures that the created DbContext instances are aware of the current tenant context. +/// +[UsedImplicitly] +public class TenantAwareDbContextFactory( + IDbContextFactory decoratedFactory, + ITenantAccessor tenantAccessor) : IDbContextFactory + where TDbContext : DbContext +{ + public TDbContext CreateDbContext() + { + var context = decoratedFactory.CreateDbContext(); + SetTenantId(context); + return context; + } + + public async Task CreateDbContextAsync(CancellationToken cancellationToken = default) + { + var context = await decoratedFactory.CreateDbContextAsync(cancellationToken); + SetTenantId(context); + return context; + } + + private void SetTenantId(TDbContext context) + { + if (context is ElsaDbContextBase elsaContext) + elsaContext.TenantId = tenantAccessor.Tenant?.Id.NullIfEmpty(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowInstanceStore.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowInstanceStore.cs index 79bee6bb7..69ca5afd5 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowInstanceStore.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowInstanceStore.cs @@ -1,10 +1,11 @@ using System.Diagnostics.CodeAnalysis; +using Elsa.Common; +using Elsa.Common.Codecs; using Elsa.Common.Entities; using Elsa.Common.Models; using Elsa.Extensions; using Elsa.Workflows; using Elsa.Workflows.Management; -using Elsa.Workflows.Management.Compression; using Elsa.Workflows.Management.Entities; using Elsa.Workflows.Management.Filters; using Elsa.Workflows.Management.Models; diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/ActivityExecutionLogStore.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/ActivityExecutionLogStore.cs index 08229ccf9..43cb9fec6 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/ActivityExecutionLogStore.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/ActivityExecutionLogStore.cs @@ -1,9 +1,9 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using Elsa.Common; +using Elsa.Common.Codecs; using Elsa.Extensions; using Elsa.Workflows; -using Elsa.Workflows.Management; -using Elsa.Workflows.Management.Compression; using Elsa.Workflows.Management.Options; using Elsa.Workflows.Runtime; using Elsa.Workflows.Runtime.Entities; diff --git a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs index 0714ca238..698e1412c 100644 --- a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs +++ b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs @@ -27,7 +27,7 @@ public record ObjectConverterOptions(JsonSerializerOptions? SerializerOptions = /// public static class ObjectConverter { - public static bool StrictMode = true; // Set to false to revert to original flexible behavior. + public static bool StrictMode = false; // Set to true to opt into strict mode. /// /// Attempts to convert the source value into the destination type. diff --git a/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariablesAndInputOutputAccessors.cs b/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariablesAndInputOutputAccessors.cs index 14e407125..479000b11 100644 --- a/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariablesAndInputOutputAccessors.cs +++ b/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariablesAndInputOutputAccessors.cs @@ -55,7 +55,7 @@ public class ConfigureEngineWithVariablesAndInputOutputAccessors(IOptions x.Name.IsValidVariableName()).ToDictionary(x => x.Name); + var inputs = context.GetWorkflowInputs().Where(x => x.Name.IsValidVariableName()).ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase); if (!context.TryGetWorkflowExecutionContext(out var workflowExecutionContext)) return; diff --git a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs index 94fdf51b6..256b86bae 100644 --- a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs +++ b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs @@ -32,8 +32,8 @@ public class JintJavaScriptEvaluator(IConfiguration configuration, INotification public async Task EvaluateAsync(string expression, Type returnType, ExpressionExecutionContext context, - ExpressionEvaluatorOptions? options = default, - Action? configureEngine = default, + ExpressionEvaluatorOptions? options = null, + Action? configureEngine = null, CancellationToken cancellationToken = default) { var engine = await GetConfiguredEngine(configureEngine, context, options, cancellationToken); @@ -46,7 +46,7 @@ public class JintJavaScriptEvaluator(IConfiguration configuration, INotification private async Task GetConfiguredEngine(Action? configureEngine, ExpressionExecutionContext context, ExpressionEvaluatorOptions? options, CancellationToken cancellationToken) { - options ??= new ExpressionEvaluatorOptions(); + options ??= new(); var engineOptions = new Jint.Options { @@ -130,7 +130,7 @@ public class JintJavaScriptEvaluator(IConfiguration configuration, INotification { var prepareOptions = new ScriptPreparationOptions { - ParsingOptions = new ScriptParsingOptions + ParsingOptions = new() { AllowReturnOutsideFunction = true } diff --git a/src/modules/Elsa.MassTransit/Activities/MessageReceived.cs b/src/modules/Elsa.MassTransit/Activities/MessageReceived.cs index 905bc9d53..631486075 100644 --- a/src/modules/Elsa.MassTransit/Activities/MessageReceived.cs +++ b/src/modules/Elsa.MassTransit/Activities/MessageReceived.cs @@ -16,48 +16,63 @@ public class MessageReceived : Trigger internal const string InputKey = "Message"; /// - public MessageReceived([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + public MessageReceived([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line) { } /// /// The message type to receive. /// - public Type MessageType { get; set; } = default!; + public Type MessageType { get; set; } = null!; /// protected override object GetTriggerPayload(TriggerIndexingContext context) => GetBookmarkPayload(context.ExpressionExecutionContext); /// - protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + protected override ValueTask ExecuteAsync(ActivityExecutionContext context) { - // If we did not receive external input, it means we are just now encountering this activity and we need to block execution by creating a bookmark. - if (!context.TryGetWorkflowInput(InputKey, out var message) || message.GetType() != MessageType) + // If we did not receive external input, it means we are just now encountering this activity, and we need to block execution by creating a bookmark. + if (!TryGetMessage(context, out var message)) { // Create bookmarks for when we receive the expected HTTP request. context.CreateBookmark(GetBookmarkPayload(context.ExpressionExecutionContext), ResumeAsync, includeActivityInstanceId: false); - return; + return default; } - // Provide the received message as output. - context.Set(Result, message); - - // Remove the input to prevent it from being passed to the next activity. - context.WorkflowInput.Remove(InputKey); - - // Complete. - await context.CompleteActivityAsync(); + return ExecuteInternalAsync(context, message); } private ValueTask ResumeAsync(ActivityExecutionContext context) { + if(!TryGetMessage(context, out var message)) + throw new InvalidOperationException("Message was not received."); + return ExecuteInternalAsync(context, message); + } + + private ValueTask ExecuteInternalAsync(ActivityExecutionContext context, object message) + { + // Provide the received message as output. + context.Set(Result, message); + // Remove the input to prevent it from being passed to the next activity. context.WorkflowInput.Remove(InputKey); - + // Complete. return context.CompleteActivityAsync(); } + private bool TryGetMessage(ActivityExecutionContext context, out object message) + { + if (!context.TryGetWorkflowInput(InputKey, out message)) + return false; + + if (message.GetType() == MessageType) + return true; + + message = null!; + return false; + } + private object GetBookmarkPayload(ExpressionExecutionContext context) { // Generate bookmark data for message type. diff --git a/src/modules/Elsa.MassTransit/Features/MassTransitFeature.cs b/src/modules/Elsa.MassTransit/Features/MassTransitFeature.cs index 66780b0e0..39870a8ad 100644 --- a/src/modules/Elsa.MassTransit/Features/MassTransitFeature.cs +++ b/src/modules/Elsa.MassTransit/Features/MassTransitFeature.cs @@ -81,7 +81,7 @@ public class MassTransitFeature : FeatureBase var category = categoryAttr?.Category ?? activityAttr?.Category ?? "MassTransit"; var descriptionAttr = messageType.GetCustomAttribute(); var description = descriptionAttr?.Description ?? activityAttr?.Description; - options.VariableDescriptors.Add(new VariableDescriptor(messageType, category, description)); + options.VariableDescriptors.Add(new(messageType, category, description)); } }); } diff --git a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs index ffafd87cb..5759abed5 100644 --- a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs +++ b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs @@ -43,27 +43,27 @@ public class WorkflowsFeature : FeatureBase public WorkflowsFeature(IModule module) : base(module) { } - + /// /// A factory that instantiates a concrete . /// public Func StandardInStreamProvider { get; set; } = _ => new StandardInStreamProvider(Console.In); - + /// /// A factory that instantiates a concrete . /// public Func StandardOutStreamProvider { get; set; } = _ => new StandardOutStreamProvider(Console.Out); - + /// /// A factory that instantiates a concrete . /// public Func IdentityGenerator { get; set; } = sp => new RandomLongIdentityGenerator(); - + /// /// A handler for committing workflow execution state. /// public Func CommitStateHandler { get; set; } = sp => new NoopCommitStateHandler(); - + /// /// A factory that instantiates a concrete . /// @@ -80,7 +80,7 @@ public class WorkflowsFeature : FeatureBase public Action WorkflowExecutionPipeline { get; set; } = builder => builder .UseExceptionHandling() .UseDefaultActivityScheduler(); - + /// /// A delegate to configure the . /// @@ -94,7 +94,7 @@ public class WorkflowsFeature : FeatureBase StandardInStreamProvider = provider; return this; } - + /// /// Fluent method to set . /// diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExceptionHandlingMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExceptionHandlingMiddleware.cs index b7028e2b8..efe703810 100644 --- a/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExceptionHandlingMiddleware.cs +++ b/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExceptionHandlingMiddleware.cs @@ -32,14 +32,34 @@ public class ExceptionHandlingMiddleware(ActivityMiddlewareDelegate next, IIncid catch (Exception e) { logger.LogWarning(e, "An exception was caught from a downstream middleware component"); - context.Fault(e); + LogExceptionAndTransition(context, e); + FaultAncestors(context); await HandleIncidentAsync(context); } } + private void LogExceptionAndTransition(ActivityExecutionContext context, Exception e) + { + context.Exception = e; + context.TransitionTo(ActivityStatus.Faulted); + var activity = context.Activity; + var exceptionState = ExceptionState.FromException(e); + var now = systemClock.UtcNow; + var incident = new ActivityIncident(activity.Id, activity.NodeId ,activity.Type, e.Message, exceptionState, now); + context.WorkflowExecutionContext.Incidents.Add(incident); + } + private async Task HandleIncidentAsync(ActivityExecutionContext context) { var strategy = await incidentStrategyResolver.ResolveStrategyAsync(context); strategy.HandleIncident(context); } + + private static void FaultAncestors(ActivityExecutionContext context) + { + var ancestors = context.GetAncestors(); + + foreach (var ancestor in ancestors) + ancestor.TransitionTo(ActivityStatus.Faulted); + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs index bbed2aded..f874bbeac 100644 --- a/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs +++ b/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs @@ -10,27 +10,13 @@ using Elsa.Workflows.Memory; using Elsa.Workflows.Models; using Elsa.Workflows.UIHints; using Humanizer; +using Container = Elsa.Workflows.Activities.Container; namespace Elsa.Workflows; /// -public class ActivityDescriber : IActivityDescriber +public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolver, IActivityFactory activityFactory, IPropertyUIHandlerResolver propertyUIHandlerResolver) : IActivityDescriber { - //private readonly IPropertyOptionsResolver _optionsResolver; - private readonly IPropertyDefaultValueResolver _defaultValueResolver; - private readonly IActivityFactory _activityFactory; - private readonly IPropertyUIHandlerResolver _propertyUIHandlerResolver; - /// - /// Constructor. - /// - public ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolver, IActivityFactory activityFactory, IPropertyUIHandlerResolver propertyUIHandlerResolver) - { - //_optionsResolver = optionsResolver; - _defaultValueResolver = defaultValueResolver; - _activityFactory = activityFactory; - _propertyUIHandlerResolver = propertyUIHandlerResolver; - } - /// public async Task DescribeActivityAsync([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType, CancellationToken cancellationToken = default) { @@ -90,14 +76,14 @@ public class ActivityDescriber : IActivityDescriber Ports = allPorts.ToList(), Inputs = (await DescribeInputPropertiesAsync(inputProperties, cancellationToken)).ToList(), Outputs = (await DescribeOutputPropertiesAsync(outputProperties, cancellationToken)).ToList(), - IsContainer = typeof(IContainer).IsAssignableFrom(activityType), + IsContainer = typeof(Container).IsAssignableFrom(activityType), IsBrowsable = browsableAttr == null || browsableAttr.Browsable, IsStart = isStart, IsTerminal = isTerminal, Attributes = attributes, Constructor = context => { - var activity = _activityFactory.Create(activityType, context); + var activity = activityFactory.Create(activityType, context); activity.Type = fullTypeName; return activity; } @@ -158,7 +144,7 @@ public class ActivityDescriber : IActivityDescriber if (wrappedPropertyType.IsNullableType()) wrappedPropertyType = wrappedPropertyType.GetTypeOfNullable(); - var uiSpecification = await _propertyUIHandlerResolver.GetUIPropertiesAsync(propertyInfo, null, cancellationToken); + var uiSpecification = await propertyUIHandlerResolver.GetUIPropertiesAsync(propertyInfo, null, cancellationToken); return new InputDescriptor ( @@ -172,7 +158,7 @@ public class ActivityDescriber : IActivityDescriber descriptionAttribute?.Description ?? inputAttribute?.Description, inputAttribute?.Category, inputAttribute?.Order ?? 0, - _defaultValueResolver.GetDefaultValue(propertyInfo), + defaultValueResolver.GetDefaultValue(propertyInfo), inputAttribute?.DefaultSyntax, inputAttribute?.IsReadOnly ?? false, inputAttribute?.IsBrowsable ?? true, diff --git a/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs b/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs index 71bf63fe0..76e53ea19 100644 --- a/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs +++ b/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; using Elsa.Caching.Features; +using Elsa.Common.Codecs; using Elsa.Common.Features; using Elsa.Expressions.Contracts; using Elsa.Extensions; @@ -14,10 +15,8 @@ using Elsa.Features.Services; using Elsa.Workflows.Features; using Elsa.Workflows.LogPersistence; using Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity; -using Elsa.Workflows.Management.Compression; using Elsa.Workflows.Management.Contracts; using Elsa.Workflows.Management.Entities; -using Elsa.Workflows.Management.Handlers; using Elsa.Workflows.Management.Handlers.Notifications; using Elsa.Workflows.Management.Mappers; using Elsa.Workflows.Management.Materializers; @@ -35,9 +34,10 @@ namespace Elsa.Workflows.Management.Features; /// /// Installs and configures the workflow management feature. /// +[DependsOn(typeof(CompressionFeature))] [DependsOn(typeof(MediatorFeature))] -[DependsOn(typeof(SystemClockFeature))] [DependsOn(typeof(MemoryCacheFeature))] +[DependsOn(typeof(SystemClockFeature))] [DependsOn(typeof(WorkflowsFeature))] [DependsOn(typeof(WorkflowDefinitionsFeature))] [DependsOn(typeof(WorkflowInstancesFeature))] @@ -234,10 +234,6 @@ public class WorkflowManagementFeature(IModule module) : FeatureBase(module) .AddScoped() .AddSingleton() .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() ; Services diff --git a/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs b/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs index 79a7d8ec6..c1c3e491c 100644 --- a/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs +++ b/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs @@ -4,6 +4,7 @@ using Elsa.Expressions.Helpers; using Elsa.Extensions; using Elsa.Workflows.Memory; using Elsa.Workflows.Models; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Elsa.Workflows.Management.Mappers; @@ -11,7 +12,7 @@ namespace Elsa.Workflows.Management.Mappers; /// /// Maps s to s and vice versa. /// -public class VariableDefinitionMapper(IWellKnownTypeRegistry wellKnownTypeRegistry, ILogger logger) +public class VariableDefinitionMapper(IWellKnownTypeRegistry wellKnownTypeRegistry, IServiceScopeFactory scopeFactory, ILogger logger) { /// /// Maps a to a . @@ -47,7 +48,7 @@ public class VariableDefinitionMapper(IWellKnownTypeRegistry wellKnownTypeRegist }); } - variable.StorageDriverType = !string.IsNullOrEmpty(source.StorageDriverTypeName) ? Type.GetType(source.StorageDriverTypeName) : null; + variable.StorageDriverType = GetStorageDriverType(source.StorageDriverTypeName); return variable; } @@ -90,4 +91,28 @@ public class VariableDefinitionMapper(IWellKnownTypeRegistry wellKnownTypeRegist /// Maps a list of s to a list of s. /// public IEnumerable Map(IEnumerable? source) => source?.Select(Map) ?? []; + + private Type? GetStorageDriverType(string? storageDriverTypeName) + { + if (string.IsNullOrEmpty(storageDriverTypeName)) + return null; + + var type = Type.GetType(storageDriverTypeName); + + if (type != null) + return type; + + // TODO: The following code handles backward compatibility with variable definitions referencing older .NET type namespaces. + // We will refactor this by storing a driver identifier rather than its full type name - which is brittle in case we move namespaces. + using var scope = scopeFactory.CreateScope(); + var storageDrivers = scope.ServiceProvider.GetServices(); + foreach (var storageDriver in storageDrivers) + { + var typeName = storageDriver.GetType().Name; + if (storageDriverTypeName.Contains(typeName, StringComparison.OrdinalIgnoreCase)) + return storageDriver.GetType(); + } + + return null; + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Models/VariableDescriptor.cs b/src/modules/Elsa.Workflows.Management/Models/VariableDescriptor.cs index f5439b5f9..f3376d6fe 100644 --- a/src/modules/Elsa.Workflows.Management/Models/VariableDescriptor.cs +++ b/src/modules/Elsa.Workflows.Management/Models/VariableDescriptor.cs @@ -3,4 +3,21 @@ namespace Elsa.Workflows.Management.Models; /// /// Represents a description of a .NET type that can be used as a workflow variable. /// -public record VariableDescriptor(Type Type, string Category, string? Description); \ No newline at end of file +public record VariableDescriptor(Type Type, string Category, string? Description) +{ + public class VariableDescriptorComparer : IEqualityComparer + { + public bool Equals(VariableDescriptor? x, VariableDescriptor? y) + { + if (ReferenceEquals(x, y)) return true; + if (x is null || y is null) return false; + + return x.Type == y.Type; + } + + public int GetHashCode(VariableDescriptor obj) + { + return obj.Type.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Options/ManagementOptions.cs b/src/modules/Elsa.Workflows.Management/Options/ManagementOptions.cs index 86b8d332b..aca77eb19 100644 --- a/src/modules/Elsa.Workflows.Management/Options/ManagementOptions.cs +++ b/src/modules/Elsa.Workflows.Management/Options/ManagementOptions.cs @@ -16,7 +16,7 @@ public class ManagementOptions /// /// A collection of types that are available to the system as variable types. /// - public HashSet VariableDescriptors { get; set; } = new(); + public HashSet VariableDescriptors { get; set; } = new(new VariableDescriptor.VariableDescriptorComparer()); /// /// The format to use for compressing workflow state. diff --git a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowClient.cs b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowClient.cs index a9468d11d..d483819a5 100644 --- a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowClient.cs +++ b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowClient.cs @@ -31,8 +31,27 @@ public class DistributedWorkflowClient( public async Task CreateAndRunInstanceAsync(CreateAndRunWorkflowInstanceRequest request, CancellationToken cancellationToken = default) { - var result = await _localWorkflowClient.CreateAndRunInstanceAsync(request, cancellationToken); - return result; + var createRequest = new CreateWorkflowInstanceRequest + { + Properties = request.Properties, + CorrelationId = request.CorrelationId, + Name = request.Name, + Input = request.Input, + WorkflowDefinitionHandle = request.WorkflowDefinitionHandle, + ParentId = request.ParentId + }; + var workflowInstance = await _localWorkflowClient.CreateInstanceInternalAsync(createRequest, cancellationToken); + + // We need to lock newly created workflow instances too, because it might dispatch child workflows that attempt to resume the parent workflow. + // For example, when using a DispatchWorkflow activity configured to wait for the dispatched workflow to complete. + return await WithLockAsync(async () => await _localWorkflowClient.RunInstanceAsync(workflowInstance, new() + { + Input = request.Input, + Variables = request.Variables, + Properties = request.Properties, + TriggerActivityId = request.TriggerActivityId, + ActivityHandle = request.ActivityHandle + }, cancellationToken)); } public async Task CancelAsync(CancellationToken cancellationToken = default) diff --git a/src/modules/Elsa.Workflows.Runtime/Providers/ClrWorkflowsProvider.cs b/src/modules/Elsa.Workflows.Runtime/Providers/ClrWorkflowsProvider.cs index 25dd73e49..e76ffb413 100644 --- a/src/modules/Elsa.Workflows.Runtime/Providers/ClrWorkflowsProvider.cs +++ b/src/modules/Elsa.Workflows.Runtime/Providers/ClrWorkflowsProvider.cs @@ -1,4 +1,5 @@ using Elsa.Common.Multitenancy; +using Elsa.Extensions; using Elsa.Workflows.Management.Materializers; using Elsa.Workflows.Runtime.Features; using Elsa.Workflows.Runtime.Options; @@ -46,7 +47,7 @@ public class ClrWorkflowsProvider( { Id = id, DefinitionId = definitionId, - TenantId = tenantId + TenantId = tenantId?.NullIfEmpty() }; var materializerContext = new ClrWorkflowMaterializerContext(workflowBuilder.GetType()); diff --git a/src/modules/Elsa.Workflows.Runtime/Services/BookmarkQueueSignaler.cs b/src/modules/Elsa.Workflows.Runtime/Services/BookmarkQueueSignaler.cs index d97b76cd1..3ab6af9cc 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/BookmarkQueueSignaler.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/BookmarkQueueSignaler.cs @@ -37,7 +37,7 @@ public class BookmarkQueueSignaler : IBookmarkQueueSignaler lock (_lock) { // Reset the TCS for the next wait - _tcs = new TaskCompletionSource(); + _tcs = new(); } } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs index 6d9a182f8..223468d2c 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs @@ -104,7 +104,7 @@ public class LocalWorkflowClient( return workflowInstanceManager.ExistsAsync(workflowInstanceId, cancellationToken); } - private async Task RunInstanceAsync(WorkflowInstance workflowInstance, RunWorkflowInstanceRequest request, CancellationToken cancellationToken = default) + public async Task RunInstanceAsync(WorkflowInstance workflowInstance, RunWorkflowInstanceRequest request, CancellationToken cancellationToken = default) { var workflowState = workflowInstance.WorkflowState; diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Tests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Tests.cs index c1e228222..20704f1b7 100644 --- a/test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Tests.cs +++ b/test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Tests.cs @@ -8,6 +8,11 @@ namespace Elsa.Workflows.Core.UnitTests.ObjectConversion; public class Tests { + public Tests() + { + ObjectConverter.StrictMode = true; + } + [Fact] public void TryConvertTo_SameType_ReturnsSuccess() {