Merge pull request #6284 from elsa-workflows/bug/hangfire

Enable customizable Hangfire job storage and deprecate obsolete APIs.
This commit is contained in:
Sipke Schoorstra 2025-01-11 19:13:11 +01:00 committed by GitHub
commit c8d7882c33
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 150 additions and 100 deletions

View file

@ -49,6 +49,7 @@
<PackageVersion Include="Grpc.Tools" Version="2.68.1"/>
<PackageVersion Include="Hangfire" Version="1.8.17"/>
<PackageVersion Include="Hangfire.MemoryStorage" Version="1.8.1.1"/>
<PackageVersion Include="Hangfire.PostgreSql" Version="1.20.10"/>
<PackageVersion Include="Hangfire.Storage.SQLite" Version="0.4.2"/>
<PackageVersion Include="Humanizer.Core" Version="2.14.1"/>
<PackageVersion Include="IronCompress" Version="1.6.3"/>
@ -112,44 +113,44 @@
<PackageVersion Include="AspNetCore.Authentication.ApiKey" Version="8.0.1"/>
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.Abstractions" Version="8.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.11" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.11" />
<PackageVersion Include="Microsoft.Data.Sqlite.Core" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyModel" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageVersion Include="Npgsql" Version="8.0.6" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
<PackageVersion Include="Oracle.EntityFrameworkCore" Version="8.23.60" />
<PackageVersion Include="Polly" Version="8.4.2" />
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageVersion Include="Refit" Version="8.0.0" />
<PackageVersion Include="Refit.HttpClientFactory" Version="8.0.0" />
<PackageVersion Include="System.Formats.Asn1" Version="8.0.1" />
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.11"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.11"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.11"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.11"/>
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.Abstractions" Version="8.0.11"/>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.11"/>
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.11"/>
<PackageVersion Include="Microsoft.Data.Sqlite.Core" Version="8.0.11"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.11"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.11"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11"/>
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1"/>
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="8.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1"/>
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1"/>
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2"/>
<PackageVersion Include="Microsoft.Extensions.DependencyModel" Version="8.0.2"/>
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1"/>
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1"/>
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="8.0.11"/>
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1"/>
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2"/>
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="8.0.1"/>
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2"/>
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0"/>
<PackageVersion Include="Npgsql" Version="8.0.6"/>
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11"/>
<PackageVersion Include="Oracle.EntityFrameworkCore" Version="8.23.60"/>
<PackageVersion Include="Polly" Version="8.4.2"/>
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2"/>
<PackageVersion Include="Refit" Version="8.0.0"/>
<PackageVersion Include="Refit.HttpClientFactory" Version="8.0.0"/>
<PackageVersion Include="System.Formats.Asn1" Version="8.0.1"/>
<PackageVersion Include="System.Text.Json" Version="8.0.5"/>
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.0"/>

View file

@ -58,6 +58,7 @@
<PackageReference Include="DistributedLock.Postgres"/>
<PackageReference Include="DistributedLock.Redis"/>
<PackageReference Include="FluentStorage.Azure.Blobs"/>
<PackageReference Include="Hangfire.PostgreSql" />
<PackageReference Include="Proto.Cluster.Kubernetes" />
<PackageReference Include="Grpc.Net.Client" />
<PackageReference Include="Proto.Persistence.Sqlite"/>

View file

@ -49,6 +49,12 @@ using Elsa.Workflows.Runtime.Distributed.Extensions;
using Elsa.Workflows.Runtime.Options;
using Elsa.Workflows.Runtime.Stores;
using Elsa.Workflows.Runtime.Tasks;
using Hangfire;
using Hangfire.MemoryStorage;
using Hangfire.PostgreSql;
using Hangfire.PostgreSql.Factories;
using Hangfire.SqlServer;
using Hangfire.Storage.SQLite;
using JetBrains.Annotations;
using Medallion.Threading.FileSystem;
using Medallion.Threading.Postgres;
@ -78,8 +84,8 @@ const bool useSignalR = false; // Disabled until Elsa Studio sends authenticated
const WorkflowRuntime workflowRuntime = WorkflowRuntime.Distributed;
const DistributedCachingTransport distributedCachingTransport = DistributedCachingTransport.MassTransit;
const MassTransitBroker massTransitBroker = MassTransitBroker.Memory;
const bool useMultitenancy = false;
const bool useTenantsFromConfiguration = false;
const bool useMultitenancy = true;
const bool useTenantsFromConfiguration = true;
const bool useAgents = false;
const bool useSecrets = false;
const bool disableVariableWrappers = false;
@ -133,7 +139,36 @@ services
});
if (useHangfire)
elsa.UseHangfire();
{
JobStorage jobStorage;
if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
{
jobStorage = new PostgreSqlStorage(new NpgsqlConnectionFactory(postgresConnectionString, new()
{
QueuePollInterval = TimeSpan.FromSeconds(1)
}));
}
else if (sqlDatabaseProvider == SqlDatabaseProvider.Sqlite)
{
jobStorage = new SQLiteStorage(sqliteConnectionString, new()
{
QueuePollInterval = TimeSpan.FromSeconds(1)
});
}
else if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer)
{
jobStorage = new SqlServerStorage(sqlServerConnectionString, new()
{
QueuePollInterval = TimeSpan.FromSeconds(1)
});
}
else
{
jobStorage = new MemoryStorage();
}
elsa.UseHangfire(hangfire => hangfire.UseJobStorage(jobStorage));
}
elsa
.AddActivitiesFrom<Program>()

View file

@ -2,14 +2,7 @@
"Logging": {
"LogLevel": {
"Default": "Warning",
"Elsa": "Warning",
"MassTransit": "Warning",
"Microsoft.Extensions.Http": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore": "Warning",
"Microsoft.AspNetCore": "Warning",
"Quartz": "Warning",
"System.Net.Http": "Warning"
"Microsoft.Hosting.Lifetime": "Information"
}
},
"HostBuilder": {

View file

@ -8,14 +8,14 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Hangfire" />
<PackageReference Include="Hangfire.MemoryStorage" />
<PackageReference Include="Hangfire.Storage.SQLite" />
<PackageReference Include="Hangfire"/>
<PackageReference Include="Hangfire.MemoryStorage"/>
<PackageReference Include="Hangfire.Storage.SQLite"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\common\Elsa.Features\Elsa.Features.csproj" />
<ProjectReference Include="..\Elsa.Scheduling\Elsa.Scheduling.csproj" />
<ProjectReference Include="..\..\common\Elsa.Features\Elsa.Features.csproj"/>
<ProjectReference Include="..\Elsa.Scheduling\Elsa.Scheduling.csproj"/>
</ItemGroup>
</Project>

View file

@ -25,6 +25,7 @@ public static class ModuleExtensions
/// <summary>
/// Configures Hangfire to use SQL Server storage. Only use this feature if you are not configuring Hangfire yourself.
/// </summary>
[Obsolete("Configure storage directly on the HangfireFeature.")]
public static HangfireFeature UseSqlServerStorage(this HangfireFeature feature, Action<HangfireSqlServerStorageFeature> configure)
{
feature.Module.Use(configure);
@ -34,6 +35,7 @@ public static class ModuleExtensions
/// <summary>
/// Configures Hangfire to use SQLite storage. Only use this feature if you are not configuring Hangfire yourself.
/// </summary>
[Obsolete("Configure storage directly on the HangfireFeature.")]
public static HangfireFeature UseSqliteStorage(this HangfireFeature feature, Action<HangfireSqliteStorageFeature> configure)
{
feature.Module.Use(configure);

View file

@ -1,5 +1,7 @@
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Workflows.Runtime.Features;
using Hangfire;
using Hangfire.MemoryStorage;
using Newtonsoft.Json;
@ -9,35 +11,63 @@ namespace Elsa.Hangfire.Features;
/// <summary>
/// Sets up Hangfire. If you're setting up Hangfire yourself, then you should not enable this feature.
/// </summary>
public class HangfireFeature : FeatureBase
[DependsOn(typeof(WorkflowRuntimeFeature))] // Ensure that the workflow runtime feature's hosted services have executed before Hangfire Server starts.
public class HangfireFeature(IModule module) : FeatureBase(module)
{
/// <inheritdoc />
public HangfireFeature(IModule module) : base(module)
{
}
/// <summary>
/// A delegate that configures Hangfire.
/// </summary>
public Action<IServiceProvider, IGlobalConfiguration> ConfigureHangfire { get; set; } = (_, cfg) => cfg.UseMemoryStorage();
private Action<IServiceProvider, IGlobalConfiguration> _configureHangfire = (_, _) => { };
/// <summary>
/// A delegate that configures Hangfire's background job server options.
/// </summary>
public Action<IServiceProvider, BackgroundJobServerOptions> ConfigureBackgroundServerOptions { get; set; } = (_, _) => { };
private Action<IServiceProvider, BackgroundJobServerOptions> _configureBackgroundServerOptions = (_, _) => { };
/// <summary>
/// A delegate that creates a job storage instance.
/// </summary>
public Func<JobStorage> CreateJobStorage { get; set; } = () => new MemoryStorage();
private Func<JobStorage> _createJobStorage = () => new MemoryStorage();
/// <summary>
/// Configures Hangfire.
/// </summary>
public HangfireFeature ConfigureHangfire(Action<IServiceProvider, IGlobalConfiguration> configure)
{
_configureHangfire += configure;
return this;
}
/// <summary>
/// Configures Hangfire's background job server options.
/// </summary>
public HangfireFeature ConfigureBackgroundServerOptions(Action<IServiceProvider, BackgroundJobServerOptions> configure)
{
_configureBackgroundServerOptions += configure;
return this;
}
public HangfireFeature UseMemoryStorage()
{
return UseJobStorage(new MemoryStorage());
}
public HangfireFeature UseJobStorage(JobStorage storage)
{
_createJobStorage = () => storage;
return this;
}
/// <inheritdoc />
public override void Apply()
{
var jobStorage = _createJobStorage();
Action<IServiceProvider, IGlobalConfiguration> configAction = (sp, cfg) =>
{
cfg.UseSimpleAssemblyNameTypeSerializer();
cfg.UseRecommendedSerializerSettings(json => json.TypeNameHandling = TypeNameHandling.Objects);
cfg.UseStorage(jobStorage);
};
Action<IServiceProvider, BackgroundJobServerOptions> serverOptionsAction = (sp, options) =>
@ -46,10 +76,10 @@ public class HangfireFeature : FeatureBase
options.SchedulePollingInterval = TimeSpan.FromSeconds(1);
};
configAction += ConfigureHangfire;
serverOptionsAction += ConfigureBackgroundServerOptions;
configAction += _configureHangfire;
serverOptionsAction += _configureBackgroundServerOptions;
Services.AddHangfire(configAction);
Services.AddHangfireServer(serverOptionsAction, CreateJobStorage());
Services.AddHangfireServer(serverOptionsAction, jobStorage);
}
}

View file

@ -11,18 +11,14 @@ namespace Elsa.Hangfire.Features;
/// Configures the Hangfire feature to use SQL Server storage. If you're setting up Hangfire yourself, then you should not enable this feature.
/// </summary>
[DependsOn(typeof(HangfireFeature))]
public class HangfireSqlServerStorageFeature : FeatureBase
[Obsolete("Configure storage directly on the HangfireFeature.")]
public class HangfireSqlServerStorageFeature(IModule module) : FeatureBase(module)
{
/// <inheritdoc />
public HangfireSqlServerStorageFeature(IModule module) : base(module)
{
}
/// <summary>
/// The connection string to use when connecting to SQL Server, or the name of the connection string.
/// </summary>
public string NameOrConnectionString { get; set; } = default!;
public string NameOrConnectionString { get; set; } = null!;
/// <summary>
/// Configures the SQL Server storage options.
/// </summary>
@ -41,13 +37,11 @@ public class HangfireSqlServerStorageFeature : FeatureBase
UseRecommendedIsolationLevel = true
};
ConfigureSqlServerStorageOptions(storageOptions);
hangfireFeature.ConfigureHangfire = (_, cfg) =>
hangfireFeature.ConfigureHangfire((_, cfg) =>
{
cfg.UseSqlServerStorage(NameOrConnectionString, storageOptions);
};
hangfireFeature.CreateJobStorage = () => new SqlServerStorage(NameOrConnectionString, storageOptions);
});
});
}
}

View file

@ -10,17 +10,13 @@ namespace Elsa.Hangfire.Features;
/// Configures the Hangfire feature to use SQLite storage. If you're setting up Hangfire yourself, then you should not enable this feature.
/// </summary>
[DependsOn(typeof(HangfireFeature))]
public class HangfireSqliteStorageFeature : FeatureBase
[Obsolete("Configure storage directly on the HangfireFeature.")]
public class HangfireSqliteStorageFeature(IModule module) : FeatureBase(module)
{
/// <inheritdoc />
public HangfireSqliteStorageFeature(IModule module) : base(module)
{
}
/// <summary>
/// The connection string to use when connecting to SQL Server, or the name of the connection string.
/// </summary>
public string NameOrConnectionString { get; set; } = default!;
public string NameOrConnectionString { get; set; } = null!;
/// <summary>
/// Configures the SQL Server storage options.
@ -38,12 +34,10 @@ public class HangfireSqliteStorageFeature : FeatureBase
};
ConfigureSqlServerStorageOptions(storageOptions);
hangfireFeature.ConfigureHangfire = (_, cfg) =>
hangfireFeature.ConfigureHangfire((_, cfg) =>
{
cfg.UseSQLiteStorage(NameOrConnectionString, storageOptions);
};
hangfireFeature.CreateJobStorage = () => new SQLiteStorage(NameOrConnectionString, storageOptions);
});
});
}
}

View file

@ -22,7 +22,7 @@ namespace Elsa.Workflows;
/// <param name="Child">The child <see cref="IActivity"/> being scheduled.</param>
/// <param name="CompletionCallback">The <see cref="ActivityCompletionCallback"/> delegate to invoke when the scheduled <see cref="Child"/> activity completes.</param>
/// <param name="Tag">An optional tag.</param>
public record ActivityCompletionCallbackEntry(ActivityExecutionContext Owner, ActivityNode Child, ActivityCompletionCallback? CompletionCallback, object? Tag = default);
public record ActivityCompletionCallbackEntry(ActivityExecutionContext Owner, ActivityNode Child, ActivityCompletionCallback? CompletionCallback, object? Tag = null);
/// <summary>
/// Provides context to the currently executing workflow.
@ -239,7 +239,7 @@ public partial class WorkflowExecutionContext : IExecutionContext
public WorkflowSubStatus SubStatus { get; internal set; }
/// The root <see cref="MemoryRegister"/> associated with the execution context.
public MemoryRegister MemoryRegister { get; private set; } = default!;
public MemoryRegister MemoryRegister { get; private set; } = null!;
/// A unique ID of the execution context.
public string Id { get; set; }
@ -406,7 +406,7 @@ public partial class WorkflowExecutionContext : IExecutionContext
/// <summary>
/// Registers a completion callback for the specified activity.
/// </summary>
internal void AddCompletionCallback(ActivityExecutionContext owner, ActivityNode child, ActivityCompletionCallback? completionCallback = default, object? tag = default)
internal void AddCompletionCallback(ActivityExecutionContext owner, ActivityNode child, ActivityCompletionCallback? completionCallback = null, object? tag = null)
{
var entry = new ActivityCompletionCallbackEntry(owner, child, completionCallback, tag);
_completionCallbackEntries.Add(entry);
@ -420,7 +420,7 @@ public partial class WorkflowExecutionContext : IExecutionContext
var entry = _completionCallbackEntries.FirstOrDefault(x => x.Owner == owner && x.Child == child);
if (entry == null)
return default;
return null;
RemoveCompletionCallback(entry);
return entry;
@ -449,25 +449,25 @@ public partial class WorkflowExecutionContext : IExecutionContext
? FindActivityByInstanceId(handle.ActivityInstanceId)
: handle.ActivityHash != null
? FindActivityByHash(handle.ActivityHash)
: default;
: null;
}
/// <summary>
/// Returns the <see cref="ActivityNode"/> with the specified activity ID from the workflow graph.
/// </summary>
public ActivityNode? FindNodeById(string nodeId) => NodeIdLookup.TryGetValue(nodeId, out var node) ? node : default;
public ActivityNode? FindNodeById(string nodeId) => NodeIdLookup.TryGetValue(nodeId, out var node) ? node : null;
/// <summary>
/// Returns the <see cref="ActivityNode"/> with the specified hash of the activity node ID from the workflow graph.
/// </summary>
/// <param name="hash">The hash of the activity node ID.</param>
/// <returns>The <see cref="ActivityNode"/> with the specified hash of the activity node ID.</returns>
public ActivityNode? FindNodeByHash(string hash) => NodeHashLookup.TryGetValue(hash, out var node) ? node : default;
public ActivityNode? FindNodeByHash(string hash) => NodeHashLookup.TryGetValue(hash, out var node) ? node : null;
/// Returns the <see cref="ActivityNode"/> containing the specified activity from the workflow graph.
public ActivityNode? FindNodeByActivity(IActivity activity)
{
return NodeActivityLookup.TryGetValue(activity, out var node) ? node : default;
return NodeActivityLookup.TryGetValue(activity, out var node) ? node : null;
}
/// Returns the <see cref="ActivityNode"/> associated with the specified activity ID.
@ -526,7 +526,7 @@ public partial class WorkflowExecutionContext : IExecutionContext
}
/// Creates a new <see cref="ActivityExecutionContext"/> for the specified activity.
public async Task<ActivityExecutionContext> CreateActivityExecutionContextAsync(IActivity activity, ActivityInvocationOptions? options = default)
public async Task<ActivityExecutionContext> CreateActivityExecutionContextAsync(IActivity activity, ActivityInvocationOptions? options = null)
{
var activityDescriptor = await ActivityRegistryLookup.FindAsync(activity) ?? throw new ActivityNotFoundException(activity.Type);
var tag = options?.Tag;
@ -568,7 +568,7 @@ public partial class WorkflowExecutionContext : IExecutionContext
public ActivityOutputRegister GetActivityOutputRegister() => TransientProperties.GetOrAdd(ActivityOutputRegistryKey, () => new ActivityOutputRegister());
/// Returns the last activity result.
public object? GetLastActivityResult() => TransientProperties.TryGetValue(LastActivityResultKey, out var value) ? value : default;
public object? GetLastActivityResult() => TransientProperties.TryGetValue(LastActivityResultKey, out var value) ? value : null;
/// Adds the specified <see cref="ActivityExecutionContext"/> to the workflow execution context.
public void AddActivityExecutionContext(ActivityExecutionContext context) => _activityExecutionContexts.Add(context);