diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml
index 189b2242c..8d2f51ad2 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 fd8a699ba..146dfd451 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 64ed51057..5036635eb 100644
--- a/src/modules/Elsa.Hangfire/Extensions/JobStorageExtensions.cs
+++ b/src/modules/Elsa.Hangfire/Extensions/JobStorageExtensions.cs
@@ -23,7 +23,7 @@ public static class JobStorageExtensions
{
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 class JobStorageExtensions
{
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 5a9f2622c..0909cba4c 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 c0bc31706..0290bb702 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 708d75a44..9520abc58 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 798d6cfd2..ab3ac2089 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 class HangfireBackgroundActivityScheduler(IBackgroundJobClient background
///
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 802555b3c..743af48e1 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 class HangfireWorkflowScheduler : IWorkflowScheduler
///
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 class HangfireWorkflowScheduler : IWorkflowScheduler
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 @@ public class HangfireWorkflowScheduler : IWorkflowScheduler
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 @@ public class HangfireWorkflowScheduler : IWorkflowScheduler
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 230460257..77cf9deae 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 @@ internal class List(IWorkflowDefinitionStore store, IWorkflowDefinitionLinker li
{
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 81f5a0d42..2af9c2334 100644
--- a/src/modules/Elsa.Workflows.Api/Serialization/ArgumentJsonConverter.cs
+++ b/src/modules/Elsa.Workflows.Api/Serialization/ArgumentJsonConverter.cs
@@ -20,18 +20,27 @@ public class ArgumentJsonConverter : JsonConverter
{
_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 jsonObject = (JsonObject)JsonSerializer.SerializeToNode(value, value.GetType(), newOptions)!;
+ 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 class ArgumentJsonConverter : JsonConverter
{
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 c2541fe79..b5082642f 100644
--- a/src/modules/Elsa.Workflows.Runtime/Services/BackgroundActivityInvoker.cs
+++ b/src/modules/Elsa.Workflows.Runtime/Services/BackgroundActivityInvoker.cs
@@ -29,10 +29,10 @@ public class BackgroundActivityInvoker(
{
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);