diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index 189b2242c0..8d2f51ad25 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -6,6 +6,7 @@ on: - 'main' - 'bug/*' - 'perf/*' + - 'patch/*' release: types: [ prereleased, published ] env: @@ -42,7 +43,7 @@ jobs: run: | if [[ "${{ github.ref }}" == refs/tags/* && "${{ github.event_name }}" == "release" && ("${{ github.event.action }}" == "published" || "${{ github.event.action }}" == "prereleased")]]; then git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* - git branch --remote --contains | grep origin/main + git branch --remote --contains | grep origin/patch/3.3.2 else git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* git branch --remote --contains | grep origin/${BRANCH_NAME} diff --git a/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantAccessor.cs b/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantAccessor.cs index fd8a699baa..146dfd451b 100644 --- a/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantAccessor.cs +++ b/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantAccessor.cs @@ -11,7 +11,7 @@ public class DefaultTenantAccessor : ITenantAccessor public Tenant? Tenant { get => CurrentTenantField.Value; - internal set => CurrentTenantField.Value = value; + private set => CurrentTenantField.Value = value; } public IDisposable PushContext(Tenant? tenant) diff --git a/src/modules/Elsa.Hangfire/Extensions/JobStorageExtensions.cs b/src/modules/Elsa.Hangfire/Extensions/JobStorageExtensions.cs index 64ed510570..5036635eb4 100644 --- a/src/modules/Elsa.Hangfire/Extensions/JobStorageExtensions.cs +++ b/src/modules/Elsa.Hangfire/Extensions/JobStorageExtensions.cs @@ -23,7 +23,7 @@ public static IEnumerable> EnumerateSchedu { scheduledJobs = api.ScheduledJobs(skip, take); - var jobs = scheduledJobs.FindAll(x => x.Value.Job.Type == typeof(RunWorkflowJob) || x.Value.Job.Type == typeof(ResumeWorkflowJob)); + var jobs = scheduledJobs.FindAll(x => x.Value.Job?.Type == typeof(RunWorkflowJob) || x.Value.Job?.Type == typeof(ResumeWorkflowJob)); foreach (var job in jobs.Where(x => (string)x.Value.Job.Args[0] == name)) yield return job; @@ -45,7 +45,7 @@ public static IEnumerable> EnumerateQueuedJ { enqueuedJobs = api.EnqueuedJobs(queueName, skip, take); - var jobs = enqueuedJobs.FindAll(x => x.Value.Job.Type == typeof(RunWorkflowJob) || x.Value.Job.Type == typeof(ResumeWorkflowJob)); + var jobs = enqueuedJobs.FindAll(x => x.Value.Job?.Type == typeof(RunWorkflowJob) || x.Value.Job?.Type == typeof(ResumeWorkflowJob)); foreach (var job in jobs.Where(x => (string)x.Value.Job.Args[0] == taskName)) yield return job; diff --git a/src/modules/Elsa.Hangfire/Jobs/ExecuteBackgroundActivityJob.cs b/src/modules/Elsa.Hangfire/Jobs/ExecuteBackgroundActivityJob.cs index 5a9f2622cc..0909cba4cb 100644 --- a/src/modules/Elsa.Hangfire/Jobs/ExecuteBackgroundActivityJob.cs +++ b/src/modules/Elsa.Hangfire/Jobs/ExecuteBackgroundActivityJob.cs @@ -1,28 +1,22 @@ +using Elsa.Common.Multitenancy; using Elsa.Workflows.Runtime; +using JetBrains.Annotations; namespace Elsa.Hangfire.Jobs; /// /// A job that executes a background activity. /// -public class ExecuteBackgroundActivityJob +[UsedImplicitly] +public class ExecuteBackgroundActivityJob(IBackgroundActivityInvoker backgroundActivityInvoker, ITenantFinder tenantFinder, ITenantAccessor tenantAccessor) { - private readonly IBackgroundActivityInvoker _backgroundActivityInvoker; - - /// - /// Initializes a new instance of the class. - /// - /// - public ExecuteBackgroundActivityJob(IBackgroundActivityInvoker backgroundActivityInvoker) - { - _backgroundActivityInvoker = backgroundActivityInvoker; - } - /// /// Executes the job. /// - public async Task ExecuteAsync(ScheduledBackgroundActivity scheduledBackgroundActivity, CancellationToken cancellationToken = default) + public async Task ExecuteAsync(ScheduledBackgroundActivity scheduledBackgroundActivity, string? tenantId, CancellationToken cancellationToken = default) { - await _backgroundActivityInvoker.ExecuteAsync(scheduledBackgroundActivity, cancellationToken); + var tenant = tenantId != null ? await tenantFinder.FindByIdAsync(tenantId, cancellationToken) : null; + using var scope = tenantAccessor.PushContext(tenant); + await backgroundActivityInvoker.ExecuteAsync(scheduledBackgroundActivity, cancellationToken); } } \ No newline at end of file diff --git a/src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs b/src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs index c0bc31706d..0290bb7028 100644 --- a/src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs +++ b/src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs @@ -1,22 +1,29 @@ -using Elsa.Scheduling; +using Elsa.Common.Multitenancy; +using Elsa.Scheduling; using Elsa.Workflows.Runtime; using Elsa.Workflows.Runtime.Messages; +using Hangfire; namespace Elsa.Hangfire.Jobs; /// /// A job that resumes a workflow. /// -public class ResumeWorkflowJob(IWorkflowRuntime workflowRuntime) +public class ResumeWorkflowJob(IWorkflowRuntime workflowRuntime, ITenantFinder tenantFinder, ITenantAccessor tenantAccessor) { /// /// Executes the job. /// - /// The name of the job. + /// A unique name for this job. /// The workflow request. + /// The ID of the current tenant scheduling this job. /// The cancellation token. - public async Task ExecuteAsync(string name, ScheduleExistingWorkflowInstanceRequest request, CancellationToken cancellationToken) + // ReSharper disable once UnusedParameter.Global + [AutomaticRetry(OnAttemptsExceeded = AttemptsExceededAction.Fail)] + public async Task ExecuteAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, string? tenantId, CancellationToken cancellationToken) { + var tenant = tenantId != null ? await tenantFinder.FindByIdAsync(tenantId, cancellationToken) : null; + using var scope = tenantAccessor.PushContext(tenant); var client = await workflowRuntime.CreateClientAsync(request.WorkflowInstanceId, cancellationToken); var runRequest = new RunWorkflowInstanceRequest { diff --git a/src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs b/src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs index 708d75a441..9520abc585 100644 --- a/src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs +++ b/src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs @@ -1,22 +1,29 @@ +using Elsa.Common.Multitenancy; using Elsa.Scheduling; using Elsa.Workflows.Runtime; using Elsa.Workflows.Runtime.Messages; +using Hangfire; namespace Elsa.Hangfire.Jobs; /// /// A job that resumes a workflow. /// -public class RunWorkflowJob(IWorkflowRuntime workflowRuntime) +public class RunWorkflowJob(IWorkflowRuntime workflowRuntime, ITenantFinder tenantFinder, ITenantAccessor tenantAccessor) { /// /// Executes the job. /// - /// The name of the job. + /// A unique name for this job. /// The workflow request. + /// The ID of the current tenant scheduling this job. /// The cancellation token. - public async Task ExecuteAsync(string name, ScheduleNewWorkflowInstanceRequest request, CancellationToken cancellationToken) + // ReSharper disable once UnusedParameter.Global + [AutomaticRetry(OnAttemptsExceeded = AttemptsExceededAction.Fail)] + public async Task ExecuteAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, string? tenantId, CancellationToken cancellationToken) { + var tenant = tenantId != null ? await tenantFinder.FindByIdAsync(tenantId, cancellationToken) : null; + using var scope = tenantAccessor.PushContext(tenant); var client = await workflowRuntime.CreateClientAsync(cancellationToken); var createAndRunRequest = new CreateAndRunWorkflowInstanceRequest { diff --git a/src/modules/Elsa.Hangfire/Services/HangfireBackgroundActivityScheduler.cs b/src/modules/Elsa.Hangfire/Services/HangfireBackgroundActivityScheduler.cs index 798d6cfd2a..ab3ac20898 100644 --- a/src/modules/Elsa.Hangfire/Services/HangfireBackgroundActivityScheduler.cs +++ b/src/modules/Elsa.Hangfire/Services/HangfireBackgroundActivityScheduler.cs @@ -1,3 +1,4 @@ +using Elsa.Common.Multitenancy; using Elsa.Hangfire.Jobs; using Elsa.Hangfire.States; using Elsa.Workflows.Runtime; @@ -9,11 +10,12 @@ namespace Elsa.Hangfire.Services; /// /// Invokes activities from a background worker within the context of its workflow instance using Hangfire. /// -public class HangfireBackgroundActivityScheduler(IBackgroundJobClient backgroundJobClient) : IBackgroundActivityScheduler +public class HangfireBackgroundActivityScheduler(IBackgroundJobClient backgroundJobClient, ITenantAccessor tenantAccessor) : IBackgroundActivityScheduler { public Task CreateAsync(ScheduledBackgroundActivity scheduledBackgroundActivity, CancellationToken cancellationToken = default) { - var jobId = backgroundJobClient.Create(x => x.ExecuteAsync(scheduledBackgroundActivity, CancellationToken.None), new PendingState()); + var tenantId = tenantAccessor.Tenant?.Id; + var jobId = backgroundJobClient.Create(x => x.ExecuteAsync(scheduledBackgroundActivity, tenantId, CancellationToken.None), new PendingState()); return Task.FromResult(jobId); } @@ -28,7 +30,8 @@ public Task ScheduleAsync(string jobId, CancellationToken cancellationToken = de /// public Task ScheduleAsync(ScheduledBackgroundActivity scheduledBackgroundActivity, CancellationToken cancellationToken = default) { - var jobId = backgroundJobClient.Enqueue(x => x.ExecuteAsync(scheduledBackgroundActivity, CancellationToken.None)); + var tenantId = tenantAccessor.Tenant?.Id; + var jobId = backgroundJobClient.Enqueue(x => x.ExecuteAsync(scheduledBackgroundActivity, tenantId, CancellationToken.None)); return Task.FromResult(jobId); } diff --git a/src/modules/Elsa.Hangfire/Services/HangfireWorkflowScheduler.cs b/src/modules/Elsa.Hangfire/Services/HangfireWorkflowScheduler.cs index 802555b3cd..743af48e1a 100644 --- a/src/modules/Elsa.Hangfire/Services/HangfireWorkflowScheduler.cs +++ b/src/modules/Elsa.Hangfire/Services/HangfireWorkflowScheduler.cs @@ -1,3 +1,4 @@ +using Elsa.Common.Multitenancy; using Elsa.Hangfire.Extensions; using Elsa.Hangfire.Jobs; using Elsa.Scheduling; @@ -9,33 +10,25 @@ namespace Elsa.Hangfire.Services; /// /// An implementation of that uses Hangfire. /// -public class HangfireWorkflowScheduler : IWorkflowScheduler +public class HangfireWorkflowScheduler( + IBackgroundJobClient backgroundJobClient, + IRecurringJobManager recurringJobManager, + ITenantAccessor tenantAccessor, + JobStorage jobStorage) : IWorkflowScheduler { - private readonly IBackgroundJobClient _backgroundJobClient; - private readonly IRecurringJobManager _recurringJobManager; - private readonly JobStorage _jobStorage; - - /// - /// Initializes a new instance of the class. - /// - public HangfireWorkflowScheduler(IBackgroundJobClient backgroundJobClient, IRecurringJobManager recurringJobManager, JobStorage jobStorage) - { - _backgroundJobClient = backgroundJobClient; - _recurringJobManager = recurringJobManager; - _jobStorage = jobStorage; - } - /// public ValueTask ScheduleAtAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, DateTimeOffset at, CancellationToken cancellationToken = default) { - _backgroundJobClient.Schedule(job => job.ExecuteAsync(taskName, request, CancellationToken.None), at); + var tenantId = tenantAccessor.Tenant?.Id; + backgroundJobClient.Schedule(job => job.ExecuteAsync(taskName, request, tenantId, CancellationToken.None), at); return ValueTask.CompletedTask; } /// public ValueTask ScheduleAtAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, DateTimeOffset at, CancellationToken cancellationToken = default) { - _backgroundJobClient.Schedule(job => job.ExecuteAsync(taskName, request, CancellationToken.None), at); + var tenantId = tenantAccessor.Tenant?.Id; + backgroundJobClient.Schedule(job => job.ExecuteAsync(taskName, request, tenantId, CancellationToken.None), at); return ValueTask.CompletedTask; } @@ -54,14 +47,16 @@ public async ValueTask ScheduleRecurringAsync(string taskName, ScheduleExistingW /// public ValueTask ScheduleCronAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, string cronExpression, CancellationToken cancellationToken = default) { - _recurringJobManager.AddOrUpdate(taskName, job => job.ExecuteAsync(taskName, request, CancellationToken.None), cronExpression); + var tenantId = tenantAccessor.Tenant?.Id; + recurringJobManager.AddOrUpdate(taskName, job => job.ExecuteAsync(taskName, request, tenantId, CancellationToken.None), cronExpression); return ValueTask.CompletedTask; } /// public ValueTask ScheduleCronAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, string cronExpression, CancellationToken cancellationToken = default) { - _recurringJobManager.AddOrUpdate(taskName, job => job.ExecuteAsync(taskName, request, CancellationToken.None), cronExpression); + var tenantId = tenantAccessor.Tenant?.Id; + recurringJobManager.AddOrUpdate(taskName, job => job.ExecuteAsync(taskName, request, tenantId, CancellationToken.None), cronExpression); return ValueTask.CompletedTask; } @@ -75,18 +70,18 @@ public ValueTask UnscheduleAsync(string taskName, CancellationToken cancellation private void DeleteJobByTaskName(string taskName) { var scheduledJobIds = GetScheduledJobIds(taskName); - foreach (var jobId in scheduledJobIds) _backgroundJobClient.Delete(jobId); + foreach (var jobId in scheduledJobIds) backgroundJobClient.Delete(jobId); var queuedJobsIds = GetQueuedJobIds(taskName); - foreach (var jobId in queuedJobsIds) _backgroundJobClient.Delete(jobId); + foreach (var jobId in queuedJobsIds) backgroundJobClient.Delete(jobId); var recurringJobIds = GetRecurringJobIds(taskName); - foreach (var jobId in recurringJobIds) _recurringJobManager.RemoveIfExists(jobId); + foreach (var jobId in recurringJobIds) recurringJobManager.RemoveIfExists(jobId); } private IEnumerable GetScheduledJobIds(string taskName) { - return _jobStorage.EnumerateScheduledJobs(taskName) + return jobStorage.EnumerateScheduledJobs(taskName) .Select(x => x.Key) .Distinct() .ToList(); @@ -94,7 +89,7 @@ private IEnumerable GetScheduledJobIds(string taskName) private IEnumerable GetQueuedJobIds(string taskName) { - return _jobStorage.EnumerateQueuedJobs("default", taskName) + return jobStorage.EnumerateQueuedJobs("default", taskName) .Select(x => x.Key) .Distinct() .ToList(); @@ -102,7 +97,7 @@ private IEnumerable GetQueuedJobIds(string taskName) private IEnumerable GetRecurringJobIds(string taskName) { - using var connection = _jobStorage.GetConnection(); + using var connection = jobStorage.GetConnection(); var jobs = connection.GetRecurringJobs().Where(x => x.Job.Type == typeof(TJob) && (string)x.Job.Args[0] == taskName); return jobs.Select(x => x.Id).Distinct().ToList(); } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/List/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/List/Endpoint.cs index 230460257f..77cf9deae9 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/List/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/List/Endpoint.cs @@ -33,7 +33,7 @@ private WorkflowDefinitionFilter CreateFilter(Request request) { var versionOptions = string.IsNullOrWhiteSpace(request.VersionOptions) ? default(VersionOptions?) : VersionOptions.FromString(request.VersionOptions); - return new WorkflowDefinitionFilter + return new() { IsSystem = request.IsSystem, VersionOptions = versionOptions, diff --git a/src/modules/Elsa.Workflows.Api/Serialization/ArgumentJsonConverter.cs b/src/modules/Elsa.Workflows.Api/Serialization/ArgumentJsonConverter.cs index 81f5a0d42e..2af9c2334a 100644 --- a/src/modules/Elsa.Workflows.Api/Serialization/ArgumentJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Api/Serialization/ArgumentJsonConverter.cs @@ -20,18 +20,27 @@ public ArgumentJsonConverter(IWellKnownTypeRegistry wellKnownTypeRegistry) { _wellKnownTypeRegistry = wellKnownTypeRegistry; } - + /// public override void Write(Utf8JsonWriter writer, ArgumentDefinition value, JsonSerializerOptions options) { var newOptions = new JsonSerializerOptions(options); newOptions.Converters.RemoveWhere(x => x is ArgumentJsonConverterFactory); - + var jsonObject = (JsonObject)JsonSerializer.SerializeToNode(value, value.GetType(), newOptions)!; - var isArray = value.Type.IsCollectionType(); - jsonObject["isArray"] = isArray; - jsonObject["type"] = _wellKnownTypeRegistry.GetAliasOrDefault(isArray ? value.Type.GetCollectionElementType() : value.Type); + var typeName = value.Type; + var typeAlias = _wellKnownTypeRegistry.TryGetAlias(typeName, out var alias) ? alias : null; + var isArray = typeName.IsArray; + var isCollection = typeName.IsCollectionType(); + var elementTypeName = isArray ? typeName.GetElementType() : isCollection ? typeName.GenericTypeArguments[0] : typeName; + var elementTypeAlias = _wellKnownTypeRegistry.GetAliasOrDefault(elementTypeName); + var isAliasedArray = (isArray || isCollection) && typeAlias != null; + var finalTypeAlias = isArray || isCollection ? typeAlias ?? elementTypeAlias : elementTypeAlias; + + if (isArray && !isAliasedArray) jsonObject["isArray"] = isArray; + if (isCollection) jsonObject["isCollection"] = isCollection; + jsonObject["type"] = finalTypeAlias; JsonSerializer.Serialize(writer, jsonObject, newOptions); } @@ -40,11 +49,12 @@ public override ArgumentDefinition Read(ref Utf8JsonReader reader, Type typeToCo { var jsonObject = (JsonObject)JsonNode.Parse(ref reader)!; var isArray = jsonObject["isArray"]?.GetValue() ?? false; - var typeName = jsonObject["type"]!.GetValue(); - var type = _wellKnownTypeRegistry.GetTypeOrDefault(typeName); + var isCollection = jsonObject["isCollection"]?.GetValue() ?? false; + var typeAlias = jsonObject["type"]!.GetValue(); + var type = _wellKnownTypeRegistry.GetTypeOrDefault(typeAlias); - if (isArray) - type = type.MakeArrayType(); + if (isArray) type = type.MakeArrayType(); + if (isCollection) type = type.MakeGenericType(type.GenericTypeArguments[0]); var newOptions = new JsonSerializerOptions(options); newOptions.Converters.RemoveWhere(x => x is ArgumentJsonConverterFactory); diff --git a/src/modules/Elsa.Workflows.Runtime/Services/BackgroundActivityInvoker.cs b/src/modules/Elsa.Workflows.Runtime/Services/BackgroundActivityInvoker.cs index c2541fe79b..b5082642fb 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/BackgroundActivityInvoker.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/BackgroundActivityInvoker.cs @@ -29,10 +29,10 @@ public async Task ExecuteAsync(ScheduledBackgroundActivity scheduledBackgroundAc { var workflowInstanceId = scheduledBackgroundActivity.WorkflowInstanceId; var workflowInstance = await workflowInstanceManager.FindByIdAsync(workflowInstanceId, cancellationToken); - if (workflowInstance == null) throw new Exception("Workflow instance not found"); + if (workflowInstance == null) throw new("Workflow instance not found"); var workflowState = workflowInstance.WorkflowState; var workflow = await workflowDefinitionService.FindWorkflowGraphAsync(workflowInstance.DefinitionVersionId, cancellationToken); - if (workflow == null) throw new Exception("Workflow definition not found"); + if (workflow == null) throw new("Workflow definition not found"); var workflowExecutionContext = await WorkflowExecutionContext.CreateAsync(serviceProvider, workflow, workflowState, cancellationToken: cancellationToken); var activityNodeId = scheduledBackgroundActivity.ActivityNodeId; var activityExecutionContext = workflowExecutionContext.ActivityExecutionContexts.First(x => x.NodeId == activityNodeId);